走进序列化之基础篇(勇气篇)
介绍:
基础篇:超多案例+超详细解析序列化后的二进制文件(以字节为单位分析)。
原理篇:JDK源码+Java-Object Serialization Specification官方文档解读。
终结篇:序列化机制之我见+翻译的序列化英文的官方文档PDF。
(基础篇称之为勇气篇,内容超多能看完确实需要勇气)
目的:
通过了解序列化机制,进一步增加对JDK源码的了解,感受JDK源码的优美。
声明:
由于排版问题,很多地方以图片形式展示,在附件中有所有案例的源代码。
目录:
1:浅谈序列化
2:走进Java序列化
第一:内置序列化机制之完全性序列化
第二:内置序列化机制之选择性序列化
第三:内置序列化机制+自定义序列化(重点)
第四:完全自定义序列化
第一部分:浅谈序列化
序列化并非只是Java语言特有的一种机制,有很多面向对象语言都支持。序列化是指将对象的状态信息转换成可存储、可传输的特定格式的数据并保存到一种临时(如主存)或者永久性(如硬盘)存储区域中的过程,当再次需要使用这个对象的时候,需要经过反序列化的过程,利用保存的对象的某时刻的状态数据和类信息(如class文件)一起恢复该对象。但是序列化并不是序列化对象,而是指序列化对象的某个状态。
无论我们使用new关键字或者Java提供的反射机制生成的对象,其初始属性值都是固定的。每次从Class文件中创建的对象从某种意义上都是一模一样(当然内存地址不同),而当创建了该对象并使用后,其实它的很多属性都已经改变,此时的对象已经不再是当初的对象,这个可以简单的理解为对象的状态。如果我们可以对某一个时刻的对象某些状态属性进行保存,类似保存成class文件一样的二进制文件,那么我们拥有的就不再只是那个每次都一模一样那个的对象了,这样该是多么美妙的一件事情。
对于它的用处,可以简单的想象一下来体会它的魅力。如果某个系统在某个时刻崩溃了,这个还是完全有可能的,假如崩溃前某一时刻系统中所有正在使用的对象们的信息已经经过序列化进行保存,那么当机器重启,我们拥有的是什么?拥有的就不再是第一次创建时候的对象们,我们以前所做的所有的工作不会因为机器故障而付之东流。
第二部分:走进序列化
1)内置序列化机制之完全性序列化(只实现Serializable接口)
对于Java中的序列化,大家估计都知道,只要实现Serializable接口就可以,尽管该接口并没有定义任何方法和属性。其实它只是一个标志,表明“我”的立场:我支持序列化。
先体验一把。
我们的代码结构图:如果用英文实在不好表示,所以出现了汉语包名
而且,每个测试都分别对应下面的案例,如Test5对应案例5。
其实这样的命名对写测试的人很不合适,因为后边案例命名都依赖于编号在它前面的测试,设想当我想在完全序列化包下添加一个新的案例的时候,其后面的案例的名字全部要更改,这个类似数组的插入,代价很大!但是为了结构清楚和讨论方便还是这样做了。
案例1:序列化“小学生类”(省略了set和get方法)
案例1测试(Test1)
运行结果:
也许这个过于简单,稍微改动一下:
案例2测试(Test2):
新的运行结果:
和你想象的是否有出入呢?命名设Id为“9999”,但是结果依旧是1009.
这个和序列化原理有关,后面会详细进行分析。
案例3:序列化很多对象
这里提示一下:如果你是序列化了很多对象,按照上面的写入没有问题,当读出的时候,按照一般流的读出会在while循环判断根据read的返回值是不是-1或者null判断,但是objectInputStream返回的既不是-1也不是null,判断会很麻烦。一个比较好的解决办法是使用容器例如ArrayList,将一个容器写入然后再将整个容器读出。
案例3测试(Test1):
运行一下:
SerializeHelper是我进行封装的一个简单的工具。
序列化:
反序列化:
第二:内置序列化机制之选择性序列化(关键字transient和使用serialPersistentFields)
总有些时候不需要或不能全部序列化。试想:
1:某敏感字段如密码不想要被序列化
2:某字段本身不能序列化
解决办法:使用关键字transient(瞬时的)或者设定serialPersistentFields
案例4:高中生总会有些秘密
案例4测试(Test4):
案例4测试结果
Password字段的状态信息没有被写入到磁盘中,当读出的时候已经默认将值为null的password转换成字符串“null”。
案例5:大学生有些字段(如导师)不能被序列化。
案例5测试(Test5):
案例5测试结果:
怎么办?不序列化它,使用transient关键字可以,此处我们采用另外一种:
案例5测试结果:
第三:内置序列化机制+自定义序列化(实现readObject和writeObject方法)
试想:
1:某字段如密码必须被序列化,但是需要用户指定序列化方式
解决办法:在被序列化类中自定义readObject和writeObject方法,该方法要求很严格,修饰符private、返回值void、参数列表ObjectInputStream或者objectOutputStream。
案例6:女硕士的年龄总是需要模糊处理一下为好
案例6测试:
案例6测试结果:25左移两位后是100
当然想要解密,则只需要在readObject中进行处理(如将上述代码取消注销)
当机密的信息经过网络传输时,可以先在本地加密(在writeObject中实施),但是在readObject中并不解密,而是传输到远方(Remote)的机器中后进行解密。中间既是被人截获readObject得到的也是假的数据。
真的可以这么随便吗?当然不是。
从现在我们开始分析序列化生成的二进制文件
首先我们使用能够打开二进制数据的软件(本人使用WinHex)打开序列化“小学生”时得到的primary2.out文件(16进制)
这个文件对应的是“完全序列化”时
然后,我们选中所有数据,拷贝出来。(自己用的不是很熟,中间拷贝的时候遇到些问题,现在把过程贴出来)
选择编辑后
我写了两个简单方法把这个很长的字符串转换成了上述格式(加上0x,表示16进制)其中反序列化时用到了该方法:
案例7测试结果:
就是这样,我们通过改变二进制文件就实现了对id属性值的改变。
借此机会,我们好好研究一下序列化生成的二进制文件,当然这个需要借助Java-Object Serialization Specification(Java对象系列化官方文档),可以在官网上下载,我已经下载好会在附件中提供。分析的时候关键标识如下图:请记住该图片我们称之为【标准图】
看不懂不要紧,下面一步一步详细进行分析
本案例说实在有点过于简单,只是写入一个对象,既然要分析,我们来点复杂的,更能说明问题。
案例8测试:
案例8生成的二进制文件:
下面我们开始第一次解读序列化后的二进制文件:注意参看【标准图】
上述一共四个操作
1:writeBoolean
2:writeObject
3:writeInt
4:writeObject
其中:
基本数据类型的开始标志是TC_ BLOCKDATA
对象的开始标志是TC_OBJECT
String开始标志是TC_STRING
【ACED0005】表示基本描述信息
[AC ED]这两个字节表示“魔数”MAGIC,和class文件相似
[00 05]这两个字节表示版本号
---------------------以下对应writeBoolean(true)--------------------------------------------------
【77 01 01】表示写进去一个长度为1字节,值为1的基本数据类型的数据。
[77]表示TC_BLOCKDATA,意思是以下是基本数据类型的数据。
[01]表示数据长度
[01]表示数据值为1(对应Boolean中的true)
---------------------以下对应writeObject(son)----------------------------------------------------
有点长,用图片标出
[73]表示TC_OBJECT ,意思是下面是一个对象
---------------------------------下面是对Son类描述信息--------------------------------------
[72]表示TC_CLASSDESC,意思是对象对应的类的描述
[ 00 11]表示类名长度为17(十六进制11代表十进制17)
[63 6F 6D 2E 77 73 63 2E 62 65 61 6E 73 2E 53 6F 6E]:“com.wsc.beans.Son”(长17)
[14 83 9E E6 B7 C2 B9 E9]表示Son中serialVersionUID,都是8个字节长
和Son中生成的一样。以后serialVersionUID就一笔带过。
[03]:表示该类支持序列化,同时又实现了WriteObject方法。这个在文档里面根本查不到,网上找遍了也没找到,自己从凌晨3点10分到4点08分,耗了我一个多小时,终于在源码中找到。截屏以纪念:汗
该类是:ObjectStreamClass 第670行到681行之间。
因为很多案例此处是02,而且在下面分析的Father和GrandFather也是02,02表示支持序列化,文档可以查到,一步一步debug后才找到。
凌晨4点14分,继续吧!!
[00 02]表示有两个属性(name和age)
[4C]表示ASCII码值“L”查看【标准图】,对应Object。
[00 03]表示用字符串来表示的长度是3(即属性age的长度)
[61 67 65]表示ASCII码:[a][g][e]
[74]表示TC_STRING意思是字符串。
[00 12]表示字符串长度是18(十六进制12表示十进制18)
[4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B]表示“Ljava/lang/String;”共计18个字符。
到此处属性age描述完毕
下面描述属性name
[4C]表示ASCII码值“L”查看【标准图】,对应Object。
[00 04]表示用字符串来表示的长度是4(即属性name的长度)
[6E 61 6D 65]表示[n][a][m][e]
[71]表示TC_REFRENCE 意思是引用
[00 7E 00 01]表示一个baseWireHadle 是一个引用地址。由于“Ljava/lang/String;”在上面已经出现过,此处只是用一个指针指向了已经存在的。这里是01,其实00表示的是第一次调用WriteObject时的Son类
【到此处说点题外话,昨天有幸去听清华大学郑纬民教授(他路径长沙)的Big Data讲座,感受了以下大家风范。他讲到清华做的一个成功的系统“Meepo”做冗余处理的时候曾经用到该策略,很多数据重复只要采取好的策略删冗还是很有效果。听的时候有点豁然开朗的感觉】
[78]表示TC_ENDBLOCKDATA 表示结束的意思,当然这只是Son类描述信息结束
----------------------------------以下是Father类的描述信息-----------------------------
[72]表示TC_CLASSDESC,意思是对象对应的类(Father类)的描述
[00 14]表示长度20
[63 6F 6D 2E 77 73 63 2E 62 65 61 6E 73 2E 46 61 74 68 65 72]对应的是“com.wsc.beans.Father”共20个字符
[13 B9 B8 7C C2 0C 74 A9]表示8个字节长度的serialVersionUID(Father类)
[02]表示支持序列化(但是没有定义readObject和WriteObject方法)
[00 01]表示有哦1个属性(即name属性)
[4C]表示ASCII中的“L”即为Object
[00 04]表示长度为4
[6E 61 6D 65]表示[n][a][m][e]
[71]表示TC_REFRENCE引用
[00 7E 00 01]表示一个baseWireHandle,是一个地址,指向了首先在Son类中描述过的“Ljava/lang/String;”
[78]我们迎来了TC_ENDBLOCKDATA,意味着Father类描述信息结束。
-----------------------------------以下是GrandFather类描述信息--------------------
[72] 表示TC_CLASSDESC,意思是对象对应的类(GrandFather类)的描述
[00 19]表示长度是25
[63 6F 6D 2E 77 …68 65 72]表示“com.wsc.beans.GrandFather”共计25个字符
[D4 41 2D CA BF FC 03 F5]相信很多人知道是8个字节长度的serialVersionUID
[02]表示支持序列化
[00 03]表示有3个属性
[49]表示I即为Int
[00 02]表示长度是2
[69 64]表示[id]
[4C]表示“L”L对应的是Object
[00 03]表示长度是3
[61 67 65]表示[a][g][e]
[71]表示TC_REFRENCE引用
[00 7E 00 01]出现了好几次了,表示引用地址,引用了“Ljava/lang/String;”
[4C]表示“L”L对应的是Object
[00 04]长度是4
[6E 61 6D 65]对应4个字符[n][a][m][e]
[71]表示TC_REFERENCE引用
[00 7E 00 01]引用地址,引用了“Ljava/lang/String;”
[78]我们迎来了TC_ENDBLOCKDATA表示GrandFather类描述信息结束
[70]表示TC_NULL意味着没有父类了。(可以看出序列化并不包含Object虽然它是根类)
---------------------以上全部是描述信息即元数据信息,下面是实例信息-----------
实例信息是倒着来赋值的,先GrandFather、Father然后是Son
[00 0D 70 70]表示GrandFather类属性值
[00 0D]由于根据描述信息,id是Int,4个字节,00 0D表示13
[70]表示TC_NULL意思是空(这里不是没有父类),表示age属性为空
[70]表示name属性为空
[70]表示Father类属性值,只有一个属性name 而且为空
[74 00 02 32 32 74 00 08 54 68 69 72 74 65 65 6E 74]表示Son类实例信息
[74]表示TC_STRING 表示字符串
[00 02]长度为2
[32 32]对应[2][2](十六进制32表示十进制50,对应ASCII中数字2)即age=“22”
[74]表示TC_STRING 表示字符串
[00 08]长度是8
[54 68 69 72 74 65 65 6E ]对应的是“Thirteen”
由于Son中定义了readObject和WriteObject方法
还有最后一个
[74 00 0C 4A 61 76 61 54 68 69 72 74 65 65 6E]表示字符串JavaThirteen
[74] 表示TC_STRING 表示字符串
[00 0C]长度是12
[4A617661546869727465656E]表示“JavaThirteen”
[78] 我们终于迎来了TC_ENDDATABLOCK 结束
[到此WriteObject(son)完毕]
[下面是writeInt(10)]
[77 04 00 00 00 0A]表示Int类型数据10
[77] TC_BLOCKDATA 基本类型数据
[04] 长度是4
[00 00 00 0A]表示10
[下面是writeObject(IamThirteenComeFromCSU)]
[74 00 16 49 61 6D 54 68 69 72 74 65 65 6E 43 6F 6D 65 46 72 6F 6D 43 53 55]
表示字符串IamThirteenComeFromCSU
[74]表示TC_STRING是指字符串
[00 16]长度是22
[49 61 6D 54 68 69 72 74 65 65 6E 43 6F 6D 65 46 72 6F 6D 43 53 55]22个字符表示为IamThirteenComeFromCSU
[到此处我们的二进制文件解释完毕]
我们对整个文件形式做个总结:如图
其实序列化文件保持这class文件的风格:紧凑。而且你会发现,关于类的信息是分为两部分年即元数据信息(描述信息)和实例数据信息,这样做是典型的面向对象思想,下面举例子会用更多信息去展示它同时也去解释一下第一节完全序列化的时候出现的问题
在第一节中有这样一个问题:
由于“小学生”案例稍微改动一下:
新的运行结果:
为什么同一对象改变了Id后再次写入实际并没有改变呢?我们可以从二进制文件中找到答案。
和原来的相比,二进制文件只增加了红笔中的5个字节
[71]表示TC_REFRENCE 引用
[71 00 7E 00 02]表示改变id为9999后写入的对象指向了编号为2的对象,下面分析一个稍微复杂点的例子,再来介绍它的编号问题。
案例9:
案例9运行结果是:
写入的第二个son2是通过new出来的,是跟第一个son不同的对象;
而当改变了第一个son的id后再次写入son后,只是添加了一个引用,这个引用指向了我们写入的第一个son(id=13),所以它的id虽然改成了9999,但是并没有起到作用,原因是因为对于一个对象只要改变了一个属性,在序列化的范围内实际上就相当于改变成另外一种状态(序列化是指序列化对象的状态),只是简单的写进去根本不行,所以当改变了属性后需要重新创建一个对象才能实现自己想要的结果。
对应的二进制文件有那些改变呢?
这个和我们分析的最详细的案例的差别在于多了上述红色标出的部分
多出来的第一部分:代表son2
[73 71 00 7E 00 00 00 00 03 E8 70 70 70 74 00 04 31 30 30 30 74 00 08 46 6F 75 72 74 65 65 6E 74 00 0C 4A 61 76 61 46 6F 75 72 74 65 65 6E 78]
多出来的第二部分:代表改变son的id为9999后写入的son
[71 00 7E 00 04]
可以发现,第一部分前5个字节[73 71 7E 00 00]和[73 71 7E 00 04]
显然[71]表示TC_REFERENE引用。
[73 71 7E 00 00]是son2的引用,引向编号为0的对象,其实这是一个ObjectStreamClass对象,是Son类型的描述信息。
[73 71 7E 00 04]是改变了id后的son的引用,引向编号为04的对象
那么序列化后编号为04的对象到底是那一个呢?如何查看呢?
最好的工具当然是Debug模式(由于本人开始不是很会用这个好东西,分析得时候很吃力,不过都过去了..)
我们先对编号的结果一睹为快:
结果是
可以清楚的看到编号是04的对象是Son。至于这个是如何编号的呢?也就是序列化的原理,稍微有点复杂,涉及到很多源代码分析,会在下一篇博客“原理篇”通过官方文档+源代码详细分析。当然可能序列化不会经常用到,如果还有心情和勇气看下去,可以看下一篇博客,可能需要点时间,写一片精致博客很耗精力的。这里稍微介绍一点到底是如何判断son是new 关键字创建的(如son2)还是本来就有的son呢?
第一步:在WriteObject方法中会先进行判断是否已经存在该对象
这里用到hash函数
它的不同之处在于不是根据hashCode来计算的,因为对hashCode方法熟悉的人知道,当我们自定义可变对象的时候,可能会重写该方法,相当于自定义比较方法(HashMap方法就是根据hashCode进行比较的)
这样判断显然不行,也别想用“equals”或者“==”,且不说正确与否,这两者需要知道两个引用即“A==B”,这里是查找(当然遍历也行)。源代码使用的是系统方法:更让人高兴的是该方法是public修饰的
通过二进制文件分析了案例7、8和9后,我们曾经试着通过修改二进制代码来改变对象,(1009改成了9999)。由于序列化后的二进制文件格式是公开的(如同class文件),如果你定义的类严重不希望也不能被别人改变,可能需要做一些措施了。
下面介绍以下这种序列化方式需要注意的事情:参考《Effective Java》
第一件事情:对于自定义对象中字段之间关系有约束条件的,需要做一些必要性检查。借用《Effective Java》书中一个案例做一下说明:
案例10:
例如自定义对象Period中需要序列化的两个字段是时间Date类型,start和end。要求start必须是小于end。
Period被定义为final,而且其字段全部是final。在构造函数中同时新建一个对象,为的就是一旦初始化该对象,就再也无法更改其属性值。当然,我们如果要求start要小于end,我们可以在构造函数中检查一下。但是要记住,序列化机制相当于为用户另开了一扇创建对象的门。Class文件相当于模版,而序列化得到的文件则是描述该对象一些信息的参数,用户有对该二进制文件修改的能力。你要做的就是检查以下用户修改后数据是否符合一些硬性要求如start小于end。其他你无能为力,如果用户把某个值从4改成5你真的没有办法检查出来。
如果只是这样,用户完全可以修改period.out文件,创建不合法的Period对象
所以需要在Period类中增加readObject方法并实现在构造函数中同样的操作:增加约束条件
这样就安全了吗?还有更加可怕的。
案例11:
虽然创建的Period在构造函数和get方法中都进行了clone,但是由于序列化机制相当于对用户开启另外一扇门,我们可以通过引用来获得start和end。只需要添加10个字节就搞定
我们先来看看案例10中的start和end对象的引用是什么,通过debug得到
现在创建一个新类:
案例11测试:
案例11测试结果
就是这样,我们通过修改二进制文件,浑水摸鱼用MutalePeriod中的两个对象start和end指向了内存中真实的对象,从而绕过了readObject中的安全性检查,任意妄为。
这个时候怎么办?需要对其进行保护性拷贝。
你拿到内存中对象又如何?我让我的start和end指向了另外一个new出来的对象,这个对象是在readObject中实现,序列化文件时(writeObject)并不会保留该对象地址,你根本拿不到
可以看出确实没保存。
案例11测试结果:
案例12:单例模式问题
由于反序列化的时候,已经通过反射机制在内存中创建了一个对象,其拿到的是根据序列化二进制文件中参数生成的一个副本,这个会破坏单例模式。
我们用一个简单的版本
案例11测试结果:
尽管是使用内存流照样不同
那么如何改变?
案例12:
在Singleton中添加4个方法,前两个很熟,后两个是新方法
案例12测试:
案例12测试结果是:
默认这4个方法都会调用。在readResolve和writeReplace返回INSTANCE即可
当然,安全是一个博大精深的问题,此处只是稍微介绍几个简单案例。
4)完全自定义序列化(实现接口Externalizable)
当然,对于用户一般也可以采用完全自主式方法实现自定义序列化,一种就是在第三种方式中WriteObject和readObject舍弃ois.defaultReadObject()和oos.defaultWriteObject()自己定义实现过程。同样还可以实现接口Externalizable。
这里只做简单介绍,分析了原理之后再介绍
案例13:上面说到女硕士,现在是男硕士
可以看出有两个public方法需要自己实现,我只是简单的实现了下密码。
案例13测试:
案例13测试结果:虽然密码是用transient修饰
关于这一节涉及东西不少,以后再具体说,不过如果对序列化了解不深或者用的很少,不建议用完全自定义。
其实自己也是菜鸟一个,很多东西有可能分析得不好,如果有错或理解不同的地方请指出!
相关推荐
内容概要:本文详细介绍了如何利用A*算法改进传统的往返式路径规划,解决扫地机器人在复杂环境中容易卡住的问题。首先构建了一个可视化的栅格地图用于模拟环境,然后引入了优先级运动规则,使机器人能够有规律地进行往返清扫。当遇到死角时,通过A*算法计算最佳逃生路径,确保机器人能够顺利脱困并继续完成清扫任务。实验结果显示,改进后的算法显著提高了清洁覆盖率,降低了路径重复率。此外,还讨论了一些潜在的优化方向,如动态调整启发函数权重、断点续传以及能耗模型等。 适合人群:对路径规划算法感兴趣的科研人员、自动化专业学生、扫地机器人开发者。 使用场景及目标:适用于需要高覆盖率和低重复率的室内清洁任务,旨在提高扫地机器人的工作效率和智能化水平。 其他说明:文中提供了详细的Matlab代码实现,并附带了仿真测试结果,有助于读者理解和复现该算法。
爬取喜马拉雅听书(1)
安卓向上传递数据学习笔记总结
1、文件说明: Centos8操作系统tigervnc-selinux-1.11.0-9.el8.rpm以及相关依赖,全打包为一个tar.gz压缩包 2、安装指令: #Step1、解压 tar -zxvf tigervnc-selinux-1.11.0-9.el8.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm
内容概要:本文详细介绍了户外储能电源双向逆变器板的技术资料及其特点。涵盖原理文件、PCB文件、源代码、电感与变压器规格参数等,适用于2KW(最大3KW)的户外储能电源。文中强调了双向软开关DC-DC设计、两颗M0+ 32位MCU的分工、SPWM调制方式、H桥IGBT的应用、详细的电气参数和技术特性。此外,还包括了SPWM信号生成代码示例、硬件设计细节、生产注意事项等。 适合人群:从事户外储能电源开发的技术人员、电子工程师、产品经理等。 使用场景及目标:帮助开发者快速掌握双向逆变器板的设计和生产要点,缩短产品研发周期,提高产品质量和可靠性。具体应用场景包括但不限于户外应急电源、便携式储能设备等。 其他说明:本文提供了丰富的技术细节和实践经验,如双向软开关DC-DC设计、SPWM调制、IGBT驱动、EMC整改记录等,有助于解决实际开发中的难题。同时,附带的实际案例展示了该方案的成功应用,进一步证明了其可行性和优越性。
电子仿真教程,从基础到精通,每个压缩包15篇教程,每篇教程5000字以上。
内容概要:美国计算机学会(ACM)是一个成立于1947年的国际性计算机专业组织,致力于推动计算机科学的发展,提供教育、资源和专业发展机会。ACM的使命是促进计算机科学和信息技术领域的进步,愿景是成为全球计算机专业人士的首选组织。其核心价值包括卓越、诚信、包容性、合作和创新。ACM定期举办学术会议,如SIGGRAPH和图灵奖颁奖典礼,出版高质量的学术期刊和会议论文集,涵盖人工智能、软件工程、网络安全等领域。此外,ACM还提供在线课程、研讨会、认证项目等教育资源,以及职业规划、网络机会和领导力培训等职业发展服务。ACM图灵奖被誉为“计算机界的诺贝尔奖”,每年颁发给对计算机科学和技术做出重大贡献的个人。; 适合人群:计算机科学领域的专业人士、教育工作者、工程师和学生。; 使用场景及目标:①了解计算机科学领域的最新研究成果和发展趋势;②获取高质量的教育资源和职业发展机会;③参与计算机科学领域的学术交流和合作。; 其他说明:ACM作为一个全球性的组织,在教育、研究和行业实践中发挥着重要作用,推动了技术创新和社会进步。
logstash-8.17.4-windows-x86_64.zip
springboot 一个基于Springboot使用Aspect实现一个切面,以记录日志为例
音箱底部折边设备sw22可编辑_三维3D设计图纸_包括零件图_机械3D图可修改打包下载_三维3D设计图纸_包括零件图_机械3D图可修改打包下载.zip
内容概要:本文详细介绍了如何使用Python、Django和MySQL构建一个完整的个性化图书推荐系统。系统从前端界面设计、后端逻辑实现到数据库设计,涵盖了用户管理、图书管理、评分系统等功能模块。重点讲解了基于用户和项目的协同过滤算法实现,以及在用户评分数据不足时的标签推荐备份方案。此外,还包括了系统部署、测试和优化的具体步骤,如云服务器部署、性能测试、数据库优化等。 适合人群:具备一定Python和Web开发基础的研发人员,尤其是对推荐系统感兴趣的技术爱好者。 使用场景及目标:适用于希望深入了解图书推荐系统的工作原理和实现细节的技术人员。目标是帮助读者掌握从零开始搭建一个完整的个性化推荐系统的方法,包括前后端开发、算法实现和系统部署。 其他说明:文中提供了大量代码示例和实战经验,如数据库设计、爬虫实现、权限管理等,有助于读者更好地理解和应用相关技术。
Ai和python学习资料
文本摘要
冲击试验机sw22_三维3D设计图纸_包括零件图_机械3D图可修改打包下载_三维3D设计图纸_包括零件图_机械3D图可修改打包下载.zip
内容概要:本文详细介绍了MyBatis Plus(MP),它是MyBatis的增强工具,旨在简化CRUD操作、提高开发效率。其主要功能包括内置分页插件、简化CRUD操作以及代码生成器。使用时只需引入相应依赖,自定义Mapper接口继承BaseMapper泛型接口,并通过实体类反射获取数据库表信息。文章还介绍了常用注解如@TableName、@TableId、@TableField、@TableLogic和@Version,配置项如全局配置、类型别名和Mapper文件路径,以及核心功能如批量插入、分页查询、条件构造器(Wrapper)等。此外,扩展功能涵盖逻辑删除、枚举处理器和JSON处理器,插件功能则包括分页插件的配置和使用。 适合人群:具备一定Java开发经验,尤其是熟悉MyBatis框架的开发者,特别是那些希望提高开发效率、减少重复代码的工作1-3年研发人员。 使用场景及目标:①简化数据库操作,提高开发效率;②快速生成代码,减少手动编写SQL语句的工作量;③实现分页查询、逻辑删除、枚举和JSON字段处理等高级功能,提升应用的灵活性和可维护性。 其他说明:本文不仅提供了MyBatis Plus的功能介绍和使用方法,还深入探讨了条件构造器(Wrapper)的使用技巧,帮助开发者更好地理解和掌握这一强大的工具。在实际开发中,合理利用这些功能可以显著提高开发效率和代码质量。建议在学习过程中结合具体项目实践,逐步掌握各个功能的应用场景和最佳实践。
电子仿真教程,从基础到精通,每个压缩包15篇教程,每篇教程5000字以上。
这个是完整源码 SpringBoot + vue 实现 【java毕业设计】Springboot+Vue高考志愿填报系统 源码+sql脚本+论文 完整版 数据库是mysql 随着高考制度的不断完善和高等教育资源的日益丰富,高考志愿填报成为考生和家长关注的焦点。本文旨在开发一个基于Spring Boot后端框架、Vue.js前端框架和实现以下功能:考生信息管理、院校信息查询、专业信息查询、志愿填报、志愿评测等。通过Spring Boot框架构建后端服务,提供 API接口与前端进行交互;Vue.js框架用于构建前端用户界面,实现数据的动态展示和交互操作;MySQL数据库用于存储考生信息、院校信息、专业信息等数据。 在系统设计过程中,我们充分考MySQL数据库的高考志愿填报系统,提高志愿填报的效率和准确性,为考生和家长提供便捷的服务。 系统主要实现以下功能:考分考MySQL数据库的高考志愿填报系统,提高志愿填报的效率和准确性,为考生和家长提供便捷的服务生信息管理、院校信息查询、专业信息查询、志愿填报、志愿评测等。通过Spring Boot框架构建后端服务,提供 API接口与前端进行交互;Vue.js框架用于构建前端用户界面,实现数据的动态展示和交互操作;MySQL数据库用于存储考生信息、院校信息、专业信息等数据。 在系统设计过程中,我们充分考虑了系统的易用性、可扩展性和安全性。通过合理的数据库设计和优化,提高了系统的查询效率。同时,采用Spring Security等安全框架对系统进行安全防护,确保数据的安全性。 本文详细阐述了系统的需求分析、设计、实现和测试过程,并对关键技术和实现难点进行了深入探讨。通过实验验证,本系统能够满足高考志愿填报的基本需求,为考生和家长提供了高效、便捷的服务。此外,本文还对系统未来的发展方向和改进空间进行了展望,以期进一步完善系统功能,提高用户体验。
内容概要:本文详细介绍了基于MATLAB实现的两种经典特征选择算法——向后搜索(SBS)和向前搜索(SFS)。首先通过构造简单的虚拟数据集展示了这两个算法的基本思想和实现步骤。接着深入探讨了SBS和SFS的具体实现方式,包括特征集的初始化、特征的选择/剔除机制以及评价函数的设计。文中还提供了具体的MATLAB代码示例,帮助读者更好地理解和应用这两种算法。此外,文章讨论了SBS和SFS的特点和局限性,并给出了在实际工程项目中的选型建议。 适合人群:对特征选择有一定兴趣并希望深入了解SBS和SFS算法的初学者,尤其是那些希望通过MATLAB进行特征选择研究的人群。 使用场景及目标:适用于需要从大量特征中挑选出最具影响力的少数特征的情况,如生物医学数据分析、图像识别等领域。主要目标是提高模型性能的同时减少计算成本。 其他说明:尽管SBS和SFS属于较为基础的特征选择方法,在现代工业级项目中已被更先进的算法所替代,但对于理解特征选择的基本原理仍然非常重要。同时,文章强调了评价函数设计的重要性,并指出在实际应用中应综合考虑业务背景和技术因素。