plainify

Lombok原理探析

前言 对于一个 Java 开发者来说,Lombok 应该是使用最多的插件之一了,他提供了一系列注解来帮助我们减轻对重复代码的编写,例如实体类中大量的 setter,getter 方法,各种 IO 流等资源的关闭、try…catch…finally 模版等,虽然可以通过 IDE 的快捷帮我们生成这些方法,但这些冗长的代码仍会影响代码的简洁性与可阅读性。 如今,随着使用者数量越来越多,Lombok 甚至成为 IDEA 的内置插件了(2020.3 版本+),可见其影响力。 但不知道使用 Lombok 的你,是否思考过这种自动生成代码究竟是什么原理呢?🤔 这些代码又是怎么产生的呢?而这就是本文要展开介绍的内容了。 Lombok 的安装与使用网络上相关的介绍已经很多了,这里就不多说了,自行查阅相关资料即可。 注解解析的两种方式 关于注解,我在之前的文章里有过详细的介绍,在解释 Lombok 的原理之前,推荐你先阅读 👉《给编译器看的注释——「注解」》 这里主要回顾一下 Lombok 注解的解析方式。 运行时解析 这是最常见的注解解析的方式,运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。 例如使用最多的@RequestMapping注解就是一个运行时注解。 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { String name() default ""; @AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {}; RequestMethod[] method() default {}; String[] params() default {}; String[] headers() default {}; String[] consumes() default {}; String[] produces() default {}; } 运行时注解的解析原理也很简单,在 java....

plainify

深入浅出序列化(2)——Kryo序列化

前一篇文章我们介绍了 Java 中的两个常见的序列化方式,JDK 序列化和 Hessian2 序列化,本文我们接着来讲述一个后起之秀——Kryo 序列化,它号称 Java 中最快的序列化框架。那么话不多说,就让我们来看看这个后起之秀到底有什么能耐吧。 Kryo 序列化 Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于 JVM 的语言上。 网上有很多资料说 Kryo 只能在 Java 上使用,这点是不对的,事实上除 Java 外,Scala 和 Kotlin 这些基于 JVM 的语言同样可以使用 Kryo 实现序列化。 和 Hessian 类似,Kryo 序列化出的结果,是其自定义的、独有的一种格式。由于其序列化出的结果是二进制的,也即 byte[],因此像 Redis 这样可以存储二进制数据的存储引擎是可以直接将 Kryo 序列化出来的数据存进去。当然你也可以选择转换成 String 的形式存储在其他存储引擎中(性能有损耗)。 由于其优秀的性能,目前 Kryo 已经成为多个知名 Java 框架的底层序列化协议,包括但不限于 👇 Apache Fluo (Kryo is default serialization for Fluo Recipes) Apache Hive (query plan serialization) Apache Spark (shuffled/cached data serialization) Storm (distributed realtime computation system, in turn used by many others) Apache Dubbo (high performance, open source RPC framework) …… 官网地址在:https://github....

plainify

深入浅出序列化(1)—— JDK序列化和Hessian序列化

我之前在《聊一聊 RPC》中曾提过什么是序列化和反序列化,当时有说过之后要单独抽出一期来详细聊聊序列化,没想到这一拖竟然拖了一年多,现在来把这个坑补上。由于篇幅较长,本文先主要介绍两种常见的序列化方式——JDK 序列化和 Hessian 序列化。 序列化是什么(What) 百度百科对于 「序列化」 的解释是: 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。 这么说太抽象了,举一个例子:你如果想让一个女孩子知道你喜欢她,你可以给她写情书,这样 「喜欢」 这种状态信息就变成了 「文字」 这种可以存储或传输的信息。 至于怎么把“情书”送给女生就有很多种方式了,我在《聊一聊 RPC》中已经有写过了,感兴趣的读者们可以点击阅读。 所以,简单理解序列化就是将“对象”存储的信息保存到某个“文件”中,之后再通过某种方式读取“文件”转换成对象。在 Java 中,序列化其实就是把一个 Java 对象变成二进制内容,本质上就是一个 byte[]数组。 既然有序列化,那么就会有反序列化,在上文的例子中,如果女孩通过情书中的文字明白了男孩的喜欢,这就是一种反序列化。在 Java 中,将一个 byte[]数组重新变成 Java 对象就是一种反序列化。 为什么要序列化(Why) 这个时候肯定就有人会问了,直接把对象作为参数传递不就可以了吗?为什么还要多此一举把对象变成“文本”,然后再将“文本”变成对象? 我们知道,Java 创建的对象都是存在于 Java 虚拟机中,也即 JVM 中,那么 JVM 又在哪里呢? JVM 是 Java 程序运行的环境,但是他同时是一个操作系统的一个应用程序,即一个进程。他的运行依赖于内存,因此 Java 中对象都是存储在内存中,准确地说是 JVM 的堆或栈内存中,可以各个线程之间进行对象传输,但是无法在进程之间进行传输。如果涉及到跨内存的数据传输(比如两台机器的传输),直接把对象作为参数传递就不可取了,这时就需要通过“网络”将数据传输。 举个例子,如果没办法自己亲自把情书送到对方手上,是不是得找一个人送过去?这就是 RPC 相关的知识了。 怎么序列化(How) 上述内容在之前那篇文章里都有涉及,接下来才是本文的重点,在实际使用时我们究竟该怎么序列化,有哪些方式可以序列化?为什么我们在代码中很少遇到手写序列化的情况。这些都是本文要解答的内容。 本文我们以 Java 为例。 JDK 序列化 作为一个成熟的编程语言,Java 本身就已经提供了序列化的方法了,因此我们也选择把他作为第一个介绍的序列化方式。 JDK 自带的序列化方式,使用起来非常方便,只需要序列化的类实现了Serializable接口即可,Serializable 接口没有定义任何方法和属性,所以只是起到了标识的作用,表示这个类是可以被序列化的。如果想把一个 Java 对象变为 byte[]数组,需要使用ObjectOutputStream。它负责把一个 Java 对象写入一个字节流:...

plainify

想追女神?先学 Synchronized 吧

在之前的《从找对象到多线程》一文中我曾介绍了一些和多线程有关的知识,而谈到多线程,就一定离不开「锁」这个名词。在 Java 中,锁的使用主要有两种:Synchronized 关键字和 Lock 接口,本文将会换个角度来聊一聊 synchronized 中的锁。 Synchronized 用的锁是存在对象头里的,用来表明当前对象所持有的锁。在 Java SE1.6 之前,Synchronized 是作为重量锁出现的,一旦使用了 synchronized,就一定会阻塞到其他线程。而在 Java SE1.6 后,为了减少获得锁和释放锁带来的性能问题,引入了"偏向锁"和"轻量锁"的概念。由此可以得知,在新的 Java 中,锁一共有 4 种状态:无锁状态、偏向锁状态、轻量锁状态和重量锁状态。这几个状态会随着竞争不断升级且只能升级不能降级,即轻量锁只会升级到重量锁而不会降级到偏向锁。 以上的解释未免太过官方了,我们从一个小例子入手。 我们用女神来表示同步代码块,就好比女神有很多追求者,同步代码块也会被很多线程执行。有一天女神的微博状态变成了「单身」,此时她就处于无锁状态,于是这些追求者纷纷创建了一个名为**「找对象」的线程**,此时对于女神(对象)来说,还没有任何线程来访问她,所以当第一个男生小 A 试图邀请她看电影的时候**(获取锁)**,她会偏向小 A 的邀请,此时她就是处于「偏向锁」状态的。有了这次经历之后,小 A 就知道该怎么邀请女神而不用反复试探了,这就是「可重入锁」,即同一个线程可以多次访问同一代码块。 再后来女神发了一条微博,说今天和这个男生看电影很开心。这条微博被其他男生看见了,大家也都知道了女神这个对象的偏向状态了。可还是有男生小 B 想追女神,此时这两个男生各自「找对象」的线程就在女神这个对象上产生了竞争。 小 B 一直关注女神的微博动态,他心想着,只要小 A 被女神拒绝了,女神就会变成「无锁」状态,自己也就有机会被女神偏向了。女神也知道小 B 在追自己,为了找到最合适的另一半,女神也在暗中观察小 B,有两个竞争者同时竞争,这时候她就处于**「轻量锁」的状态。虽然女神明显更喜欢小 A,但在小 B 心里觉得小 A 除了比自己早点出现外根本不具有和自己竞争的能力,于是不断给女神献殷勤,保持关系,这就叫自旋,**不断的将自己的时间花费在获取锁上,逐渐成为一条舔 🐶。 虽然一开始女神也会偶尔答应小 B 的邀请,但当竞争者越来越多后,小 B 变得疯狂起来,追求逐渐变成了骚扰,女神也逐渐不耐烦起来。最终在小 A 的努力下,女神和小 A 确定了关系,并发了微博告知众人,此时她的状态就升级成为**「重量锁」状态**。这时,除了小 A,其他所有竞争者的「找对象」线程都没有办法再追求女神了。这样做的好处就是赶紧断了那些追求者的念头,让他们可以早日觅得其他良人,不要在一棵树上吊死。 从线程的角度来看,重量锁使除了拥有锁的线程外的其他所有线程都阻塞,这样可以有效防止 CPU 空转,避免造成资源的浪费。 在偏向锁和轻量锁阶段,女神还没有和任何人确定关系,只要给点甜头小 B 等其他追求者都会很开心,这是一种「乐观锁」。而一旦女神和小 A 确定了关系,自身状态升级为重量锁后,小 B 就很不开心了,对他来说这就是一种「悲观锁」。...

plainify

DO,VO,DTO 你知道吗?

作为后端最常用的编程语言之一,Java 已经有很多年的历史了,在阿里内部,Java 也是使用最广泛的一门语言。在阿里实习的这段时间,规范一词是我感受最深的。没有规矩不成方圆,今天来说一下 Java 中的各种 O(bject)。 为什么会出现这些 O? 我们知道,这些 O 不管叫什么名字,其本质都还是对象(Object),既然本质都一样,为什么非要给他们套上各种马甲? 个人认为原因有三: 第一,随着编程工业化的发展,需要有一套合理的体系出现。中国人喜欢造神,外国人喜欢造概念,于是 MVC、MVP、MVVM 等编程模型就出现了,为了搭配这些编程模型的使用,需要对 Object 的功能进行划分,于是我们便看到了这些层出不穷的 Object。当然这里并没有批评这些概念的意思。 其二,我认为在团队协作编码中,一个好的命名方式是可以节约很多时间成本的。就比如getItemById一眼看去就知道是通过 id 获取一个 item 对象,ItemVO一眼看去就知道是前端透出的 json 对应的对象。 其三,如此划分,可以让项目结构更加清楚,不至于出现东一块西一块,对象乱扔的局面。尽可能避免了在多人协作时对象混乱的情况。 总的来说,这一切都是为了让软件编程更加合理、更加规范、更加高效。 有哪些 O? 这些 O 有很多衍生出的命名,比如 VO、DO、BO,这里我们把常见的 O 列举出来,然后一一解释。 以下内容参考阿里巴巴 Java 开发手册,如果有需要可以在微信公众号「01 二进制」后台回复「Java 开发手册」获得。 DO( Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。 PO(Persistant Object):持久对象,一个 PO 的数据结构对应着库中表的结构,表中的一条记录就是一个 PO 对象 DTO( Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。 BO( Business Object):业务对象。 由 Service 层输出的封装业务逻辑的对象。 AO( Application Object):应用对象。 在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。 VO( View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。 POJO( Plain Ordinary Java Object):POJO 专指只有 setter/getter/toString 的简单类,包括 DO/DTO/BO/VO 等。 DAO(Data Access Objects):数据访问对象,和上面那些 O 不同的是,其功能是用于进行数据操作的。通常不会用于描述数据实体。 一下子给出 8 个常见的 O,光看解释大家可能会有些迷糊,接下来我们从下面这张图入手,带大家直观的感受下,这些 O 的用处。...

Java 中链表与数组间的相互转换

自定义链表 ListNode 类 public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } 将链表转换成数组 //将链表转换成数组 public class Test1 { public static void main(String[] args) { Test1 test1 = new Test1(); int[] a = {1, 9, 9, 9, 9, 9, 9, 9, 9, 9}; ListNode l1 = test1.arrayToListNode(a); test1.printListNode(l1); } //数组转换成链表 public ListNode arrayToListNode(int[] s) { ListNode root = new ListNode(s[0]);//生成链表的根节点,并将数组的第一个元素的值赋给链表的根节点 ListNode other = root;//生成另一个节点,并让other指向root节点,other在此作为一个临时变量,相当于指针 for (int i = 1; i < s.length; i++) {//由于已给root赋值,所以i从1开始 ListNode temp = new ListNode(s[i]);//每循环一次生成一个新的节点,并给当前节点赋值 other.next = temp;//将other的下一个节点指向生成的新的节点 other = temp;//将other指向最后一个节点(other的下一个节点) other=other.getNext(); } return root; } /** * 遍历一个链表 */ public void printListNode(ListNode l) { while (l != null) { System.out.print(l.val + " "); l = l.next; } } } ...

257 words 2 min

Java中计算两个日期间隔多少天

String dbtime1 = "2017-02-23"; //第二个日期 String dbtime2 = "2017-02-22"; //第一个日期 //算两个日期间隔多少天 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); Date date1 = format.parse(dbtime1); Date date2 = format.parse(dbtime2); int a = (int) ((date1.getTime() - date2.getTime()) / (1000*3600*24)); 直接通过计算两个日期的毫秒数,他们的差除以一天的毫秒数,即可得到想要的两个日期相差的天数。

34 words 1 min
plainify

为什么不在 for 循环里捕获异常?

在回答标题这个问题之前,我们先试想一下,在没有 try…catch 的情况下,如果想要对函数的异常结果进行判断,我们应该怎么做? 异常 第一个想法肯定就是 if…else 了,一般情况下,相关的代码段我们都是放在一起的,如果此时你的程序中有大量的代码段要做这做判断,这就意味着后面执行的逻辑会依赖你前面语句的执行情况,也就意味着你每调用一个可能会出现错误的函数的时候,都要先判断是否成功,然后再继续执行后面的语句。这就会导致你的代码中会充斥着大量的 if…else。 Java 是一门工程性的语言,而工程也是一种艺术,因此采用这样的做法显然是很不优雅的。《Thinking in Java》中提到“badly formed code will not be run.”,意思是结构不优雅的代码不应该被执行,于是一个适用于 Java 的异常处理机制便应运而生了。 Java 的异常处理其目的在于通过使用少于目前数量的代码来简化大型程序,举个简单的例子 🌰 不用 try…catch FileReader fr = new FileReader("path"); if (fr == null) { System.err.println("Open File Error"); } else { BufferedReader br = new BufferedReader(fr); while (br.ready()) { String line = br.readLine(); if (line == null) { System.err.println("Read Line Error"); } else { System.out.println(line); } } } 用了 try…catch...