Integer、Long等自动拆箱类型为啥会抛NullPointerException

通常的NullPointerException异常其实是很好判断,一定是有某个对象是null导致的。但是java中数字都有自动装箱和拆箱功能,它简化了我们的代码,隐藏了其背后的执行步骤,导致程序抛出异常时我们会有意识的忽略了数字类型也可能会抛NullPointerException。 其实自动装箱和拆箱在字面上很容易使人对其理解的不准确或者不完整,大家都知道装箱就是从基础类型自动转换成对应的对象类型,拆箱就是从对象类型自动转换成基础类型;字面上很好理解,但是背后到底是如何实现的,其实有一种道不清说不明的感觉。自动装箱是不会抛异常的,但是自动拆箱则有可能会,所以本文从三种代码场景,以字节码的角度,还原自动拆箱背后的逻辑。

场景一:赋值时自动拆箱抛NullPointerException

上面的代码在把变量a赋值给b时就会抛出NullPointerException,使用javap Test.class命令查看其对应的字节码如下:

可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是把变量a赋值给b,显然由于a是null,所以调用longValue()方法自然会抛空指针了。

场景二:方法传参时自动拆箱抛NullPointerException

上面把变量a传参给方法increment时就会抛NullPointerException,还是用javap Test.class查看其字节码如下:

可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是真正的执行increment,显然由于a是null,所以调用longValue()方法也会抛空指针了。

场景三:用于大小比较时拆箱抛NullPointerException

同样用javap Test.class查看字节码内容如下:

同样的在行号8的字节码通过Long.longValue()方法把变量n1从Long转换成了long,然后行号8为加载变量n2,行号9才是比较两个long(注意不是比较Long)。

上面提到的字节码中的行号其实是字节码指令占用空间的偏移量,为了便于理解,暂以行号来理解

总结

拆箱的过程其实是调用了对应的XXX.xxxValue()方法,比如:Integer.intValue(), Long.longValue(), Short.shortValue(), Byte.byteValue(), Float.floatValue(), Double.doubleValue(),如果这个对象是null,那么就会抛出空指针异常。

怎样让你的代码更好的被JVM JIT Inlining

JVM JIT编译器优化技术有近100中,其中最最重要的方式就是内联(inlining)。方法内联可以省掉方法栈帧的创建,同时增加了CPU指令cache的命中率,方法内联还使让JIT编译器更多更深入的优化变成可能。本人在fastxml(速度比XPP3(基于xmlpull)还快的xml解析器)开源项目中针对方法内联进行了很多学习和实践,这里总结一下,介绍一下怎么让你的代码更好的被JVM JIT Inlining。

Inlining相关的启动参数

上一篇博客《Java JIT性能调优》中介绍了inlining相关的几个参数,这里copy下: jvm可以通过两个启动参数来控制字节码大小为多少的方法可以被内联:

  • -XX:MaxInlineSize:能被内联的方法的最大字节码大小,默认值为35B,这种方法不需要频繁的调用。比如:一般pojo类中的getter和setter方法,它们不是那种调用频率特别高的方法,但是它们的字节码大小非常短,这种方法会在执行后被内联。
  • -XX:FreqInlineSize:调用很频繁的方法能被内联的最大字节码大小,这个大小可以比MaxInlineSize大,默认值为325B(和平台有关,我的机器是64位mac)

可见,要想inlining就要让你的方法的字节码变得尽可能的小,默认情况下,你的方法要么小于35B(针对普通方法),要么小于325B(针对调用频率很高的方法)。

Inlining调优的工具

同样的上一篇博客《Java JIT性能调优》中也介绍了非常牛x的JIT优化工具JITWatch,该工具的详细文档请看这里:https://github.com/AdoptOpenJDK/jitwatch

为Inlining减少方法字节码

通过JITWatch中提示或者在启动命令中添加-XX:+PrintCompilation  -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining来输出提示信息,我们可以很快定位哪些方法需要优化,从而针对性的调优那些执行频率很高但没有inlining的方法,或者那些很小但不足以小到35Byte的方法。减少方法字节码的方法有很多,下面介绍我在fastxml项目中实践过的9种方法。

方法1. 把分支语句中的代码段抽取到方法中

我们的代码里面常常会有分支语句,比如:if-else、switch。但是我仔细想想,是不是所有的分支都有均等的执行机会,是不是很多时候,某些分支能进入的概率很小或者几乎没有。如果某个分支执行的概率很小,而且这个分支中的代码量不算小,那把这个分支中的代码抽取成一个方法就可以大大缩减当前这个方法的字节码大小。下面举一个著名NIO框架Netty中的一个例子,看优化前的代码:

代码中的config().isAutoRead()默认情况是false,也就是说,在没有专门配置的情况下,这个分支不会进去,而且一般人不会把这个配置为true,所以可以把这个if分支中的代码块抽取为方法,这样方法就变小了一些,而代码的功能没有减少。修改后的代码如下:

这样就很容易的把方法的字节码大小减少了一些,这样read()方法就可以inlining了。详见这篇文章:http://normanmaurer.me/blog_in_progress/2013/11/07/Inline-all-the-Things/

方法2.移除无用的代码

现在IDE(idea、eclipse等)功能很强大,往往能在开发阶段就能提示我们哪些代码是冗余无用的代码,请及时的删除那些无用代码,因为它们会占用方法的字节大小。此处我要说的不是这种IDE能提示我们的场景,而是隐含在我们编写的代码中的无用代码,比如下面的例子:

在方法parseTrimedString的开头处对length进行判断,乍一看,似乎没有什么不合理的,如果要处理的数组长度为0就直接返回null。逻辑上没有问题,IDE也不会提示这一段代码是无用的,但是仔细想想,要是length==0时,要是没有这个判断会怎么样呢? 第一个for循环中有begin<last的判断,避免了循环体中的bytes[begin]数组越界;同样的,第二个for循环里数组下标也被last>begin保护起来了,防止越界;再看看parseString()方法中正好有对length的判断,也就是说方法parseTrimString内部不再需要对length等于0的验证了。这一段代码可以移除。 类似这种非0、非null的判断在我们日常的代码中也是很常见的,其实可以通过一定的约定来减少这种判断,比如:按照约定保证每次方法的返回都不为null,若返回类型为String数组,则返回new String[0];若返回类型为Map<String, String>,则返回new HashMap<String, String>()。

方法3. 抽取重复的代码

很多时候,程序员因为懒惰不愿修改别人的代码,怕承担风险,而是有时候偏向于复制代码,这可能会导致一个方法体内部出现重复的表达式或者代码段。这时候应该把相同的幂等的表达式抽取为临时变量,把相同的代码段抽取为成员方法(不仅可以减少当前方法的字节码,还可能增加方法的复用率,为JIT优化提供了更多可能)。 通过抽取重复的代码来减少字节码,这种方式的效果是显而易见的。这里重点讲解一下,相同的幂等的表达式抽取为临时变量前后的字节码大小差异,看下面的代码:

这段代码中重复出现了docBytes[cursor],通常大家可能会认为这种数组取下标的操作没有几行字节码,应该不耗时,只有这种objA.getField1().getSubField2().getSubSubField3()的字节码多。其实不然,先看看仅docBytes[cursor]一句话的字节码有多少:

这里看出docBytes[cursor]对应9个字节的5行字节码,而这个表达式在上面的语句块中出现了3次,所以一共占用了9*3=27个字节。如果抽取成临时变量那么第二次和第三次使用临时变量即可,而使用临时变量仅占用1个字节,这样又可以减少近18字节。当一个方法里面重复出现的变量越多,优化效果就越明显。

方法4. 优化常量加减运算

先看代码:

上面的代码很简单,其中i=i+2这种写法也很常见,那么看看这行代码对应的字节码是什么样的:

这行代码已经很简单了,只占用4个字节,还有优化空间吗?当然有,我们还有一种常见的写法:

再看看这种写法的字节码:

这一行字节码占用3个字节的空间,而且只有一行字节码。这样一个微小的改动就可以节省1个字节,缩减3条字节码的指令条数。

方法5. 移除无用的初始化赋值

现在一些高级IDE也支持无用初始化的提示,比如:idea(eclipse还不支持这个功能)。如果IDE提示,请尽量移除这些无用的初始化,比如下面的例子:

显然对临时变量sum的初始化是没有作用的,上面的java代码产生的字节码有这些:

如果把对sum的初始化移除,java代码如下:

代码变化很小,改后的字节码如下:

通过对比可以看出移除对sum的初始化后, 字节码少了两行(2个字节)。有时候为了减少字节码就是需要一个字节一个字节的扣,没有办法。

方法6. 尽量复用临时变量

临时变量的初始化是会占用字节码的,减少不必要的临时变量无形之中也减少了临时变量的初始化。看下面的例子:

这个例子中,习惯性的使用临时变量i来作为for循环数组的下标,但是这个临时变量i真的有必要吗? 变量begin虽然是方法的参数,但是它也是这个方法的临时变量,而且它是java中的原始类型,改变begin的值不回对调用toString方法的调用方有任何影响,而且变量begin在方法中没有其他作用,很自然可以使用begin来代替i的下标作用。修改后的代码如下:

可以看到代码中少了一句:int i=begin; 这将减少2字节的2行字节码。 当你很需要这个方法被inlining时,每个字节的减少都来之不易。

方法7. 减少传参个数

有时我们写代码时,为了把方法的输入表达得更准确,会给出一个精确的参数列表,其中可能有多个参数来自相同的对象。比如下面的例子:

这代代码中,可以看到有两处抛出异常,每次创建异常都需要传三个参数:message、row、column。而上面的这个例子,很显然row和column都来自当前对象this,我在优化fastxml时,发现这个地方也可以优化一下,毕竟this.getRow()和this.getColumn()这两个表达式分别占用了4个字节,如果把formatError方法的参数减少为message、parser(即this),就可以减少7个字节的字节码,因为传this仅占用1个字节。

方法8. 把多个if判断转变成map的contain或者数组取下标操作

举个实际的案例,fastxml中为了解析xml中的标签名中的字符是否符合xml规范,我需要做如下的判断:

这一长串的条件判断其对应的字节码如下:

上面的代码中冒号前的数组为当前行字节码的第一个字节为整个方法体字节码中的位置。可以看到这里面的有11个的if判断,而每次判断都需要加载if判断的两个操作数,第一个操作数为变量b,第二个操作数为常量(比如:“:”),由于代码太长,此处以b == ‘.’为例,其字节码对应58~61处,共7个字节,三行字节码。 由于这个方法相比较的字符串都在ASCII码0~128范围内,所以我这里可以把比较转换为数组下标的方式,数组下标为byte的数值,数组的元素值为当前下标是否是符合xml规范的字符,修改后的代码如下:

可以看到方法变短来很多,字节码也减少了很多,执行速度也快很多。当然Java中原始类型比较适合用数组的方式解决这种众多if判断的问题,如果是对象的话,也可以用Map的方式。

方法9. 必要时把JDK中的类重写成简单的版本

一般而言JDK中的API都是久经考验的、非常完备的、通用的、众多高手智慧的结晶,但是也正是因为它的完备性和通用性,就导致其必然的损失了针对性。当优化到了一定程度后,就需要针对数据场景或者业务场景来进行针对性的优化来,举个例子:给一个基本排好序的数组进行排序时,冒泡比快速排序要快得多,因为这种数组最适合冒泡排序(O(n)时间复杂度),而快速排序时是O(nlog(n))。 看一个实际案例,下面的代码在fastxml的性能测试输出的编译和内联的log中看到StringBuilder的append方法太大了,导致无法内联到下面的方法中。

看看StringBuilder的append方法内容是什么样子:

可以看到append方法一层层调用了几层方法,每个方法不算太大,用JITWatch可以看到优化建议:

The call at bytecode 2 to

Class: java.lang.AbstractStringBuilder

Member: public AbstractStringBuilder append(String)

was not inlined for reason: ‘callee is too large’

The callee method is greater than the max inlining size at the C1 compiler level.

Invocations: 1031

Size of callee bytecode: 50

可以看到public AbstractStringBuilder append(String)这个方法稍大了一点,导致没有内联。这会导致多出一次方法栈,而这个方法可能在程序运行中调用频率很高,如果这个方法不能inlining,势必降低了性能。jdk的源码我们改不了,那我们可以选择不使用jdk的类,而是根据自己的实际场景写一个更简单的类,比如下面这个:

这个代码把安全检查都移除了,也把冗长繁杂的校验和数组扩容都直接移除了,代码变得简洁高效。这么做不会出错么?当然不会,因为在fastxml中,调用方已经保证了其不会出现数组越界,而且保证了初始长度length就是最大长度,所以多一层的安全检查是没有必要的,也没有必要考虑去扩容和复制数组了。在fastxml中,使用这个简化后的代码,使性能提升了10%。

总结

上面这9种方法都是在使用了JITWatch后,根据其提示绞尽脑汁想到的一些办法,通过这些优化,fastxml的性能提升了近1倍,现在fastxml的性能已经是XPP3的1倍左右。优化方法还有很多,针对不同的场景,会有很多不可思议的优化方法,这需要不断的挖掘。欢迎指正。 最后引用大牛Donald Knuth的一句话:

过早的优化是万恶之本

 

Java JIT性能调优

JVM自动监控这所有方法的执行,如果某个方法是热点方法,JVM就计划把该方法的字节码代码编译成本地机器代码,同时还会在后续的执行过程中进行可能的更深层次的优化,编译成机器代码的过程是在独立线程中执行的,不会影响程序的执行;除次以外,JVM还对热点方法和很小的方法内联到调用方的方法中,减少方法栈的创建。这些就是JIT(just in time)。

JIT编译器有近100种优化方式

JIT编译器有近百种优化方式
JIT编译器有近百种优化方式

其中以下三种方式效果非常明显:

  • 把bytecode编译成本地代码(native code):编译后的代码保存在一个特殊的堆上,除非相关的类被卸载,或者本地代码的优化被取消。这个cache有一定的大小限制(可通过启动参数-XX:ReservedCodeCacheSize来修改cache的大小),如果这个cache被装满,则JVM无法编译出更多的本地代码,但通常说不会碰到这种情况的。
    • hot method:默认情况,方法执行次数超过10000次的方法,jvm会编译成本地二进制代码,这个数值可以通过设置启动参数-XX:CompileThreshold=10000来修改。
    • On Stack Replacement (OSR):如果某个循环执行的次数非常多,那么这个循环体代码也可能会编译为本地代码
  • 分支预测(Branch Prediction):降低分支条件判断的结果的随机性,使CPU指令流水线缓存命中率提升
  • 方法内联(inlining,对性能的提升很大):方法内联可以减少方法调用,从而减少方法栈的创建。相信大家都知道循环的速度比递归快很多,就是这个原因,另外方法内联后,还使得一些JIT更深入的优化变成可能。jvm可以通过两个启动参数来控制字节码大小为多少的方法可以被内联:
    • -XX:MaxInlineSize:能被内联的方法的最大字节码大小,默认值为35Byte,这种方法不需要频繁的调用。比如:一般pojo类中的getter和setter方法,它们不是那种调用频率特别高的方法,但是它们的字节码大小非常短,这种方法会在执行后被内联。
    • -XX:FreqInlineSize:调用很频繁的方法能被内联的最大字节码大小,这个大小可以比MaxInlineSize大,默认值为325Byte(和平台有关,我的机器是64位mac)。

这些优化方法通常是层层依赖的,所以当JIT优化后的代码被JVM应用,就会开始尝试进行更上一层次的优化。因此我们写代码的时候,应该尽量往这些优化方式上面靠。

输出JIT编译和内联过的方法

在JVM启动参数中添加三个启动参数,比如下面的命令,把编译信息输出到inline.log文件中,便于后续使用grep命令分析:

inline.log中内容类似这样:

  • 第1列  31:为JVM启动后到该方法被编译相隔的时间,单位为毫秒
  • 第2列  23:编译ID,用来跟踪一个方法的编译、优化、深度优化
  • 第3列  s!:s是指该方法是synchronized,感叹号是指该方法有对异常的处理
  • 第4列  sun.misc.URLClassPath::getLoader:被编译的方法和类名
  • 第5列  (136 bytes):方法的字节码大小
  • 第6列 inline(hot):表示该方法被内联了,且调用频率很高,这一列还有其他值,比如:
    • inline (hot): 该方法被内联了,且调用频率很高
    • too big: 该方法没有被内联,因为方法字节码比-XX:MaxInlineSize的值大
    • hot method too big: 该方法被调用的频率很高,但是方法的字节码比-XX:FreqInlineSize的值大

inline.log文件内容中的方法还以tab缩进的方式来体现方法调用链的层次结构,非常易懂。

输出JIT编译的细节信息

通过添加参数-XX:+PrintCompilation,可以看到的信息其实并不具体,比如:那些方法进行了内联,内联后的二进制代码是怎么样的都没有。而要输出JIT编译的细节信息,就需要在JVM启动参数中添加这个参数:

输出的编译信息,默认情况是在启动JVM的目录下一个名为:hotspot_pid<PID>.log的文件

如果想指定文件路径和文件名的话,可以再添加一个启动参数:

输出的是一个很大的xml文件,可能有几十上百兆,下面摘出部分内容如下(文件中的汇编代码太长,就不贴了):

这些内容很难读懂,建议使用JITWatch(https://github.com/AdoptOpenJDK/jitwatch/)的可视化界面来查看JIT编译的细节信息。同时JITWatch还可以给出很多优化建议,给我们有效的优化代码提供参考,详见下文。

JIT编译模式

上面的输出的细节编译信息inline.log文件中,有个字段上“compiler=C2”,这里的C2就是JIT的编译模式,C2表示这个方法进行了深度优化。下面介绍下JIT的编译模式

C1: 通常用于那种快速启动的GUI应用,对应启动参数:-client

C2: 通常用于长时间允许的服务端应用,对应启动参数:-server

分层编译模式(tiered compilation):这是自从Java SE 7以后的新特性,可通过添加启动参数来开启:

这个特性在应用启动阶段使用C1模式以达到快速启动的效果,一旦应用程序运行起来以后,C2模式将取代C1模式,以进行更深度的优化。在Java SE 8中,这个特性是默认的。

JITWatch

前面也提到了,JITWatch可以通过可视化界面来帮助我们分析JVM输出的JIT编译输出日志,还可以帮助我们静态分析jar中的代码是否符合JIT编译优化的条件,还可以以曲线图形的方式展示JIT编译的整个过程中的一些指标,还给我们的代码提意见和建议,非常好用的工具。

下载

JITWatch需要在github上把代码clone下来,然后用maven来运行,地址为:https://github.com/AdoptOpenJDK/jitwatch/

安装hsdis

如果在jvm的启动参数中添加了下面的启动参数:

但是你发现启动你的java程序后,有如下的报错信息:

或者启动啦JITWATCH后,打开了某个编译信息log文件,但是看不到每个方法编译后的汇编信息,且那么你就需要安装hsdis。hsdis可以帮助我们查看编译后的本地代码,具体可以参考JITWatch提供的文档,根据自己的系统类型来选择安装:https://github.com/AdoptOpenJDK/jitwatch/wiki/Building-hsdis,如果你是mac,可以参考这篇文章:http://nitschinger.at/Printing-JVM-generated-Assembler-on-Mac-OS-X/

如果安装了hsdis库后,仍然在JITWatch中看不到汇编信息,那你检查下环境变量配置是否正确,实在不行可以尝试下重启电脑。

运行JITWwatch

在代码根目录下执行launchUI.sh(Linux/Mac)或则launchUI.bat(windows)

如果你使用maven,也可以在代码根目录下这样运行(其他运行方式,请参考JITWatch的github首页)

如果你使用的是mac,而且idk版本是jdk7,且运行mvn clean compile exec:java时出现下面的错误和异常时: 

请在org.adoptopenjdk.jitwatch.launch.LaunchUI类的main函数开头处添加下面的代码(或者直接使用我fork修改好的JITWatch):

然后重新运行即可看到JITWatch的界面。

用JITWatch来帮助优化代码

首先点击“Open Log”按钮,选择前面提到过的hotspot_pid<PID>.log文件,然后点击“Start”分析该文件。随后就会在左边生成程序运行过程中加载的类及其目录结构。选择某个类后,右侧会展示该类对应的方法。这些方法中可能部分方法前面有个绿颜色的勾,这说明这个方法被编译成本地代码,选中这个方法后,可以在下方看到该方法具体信息,比如方法调用次数,方法大小等。如下图所示:

jitwatch加载JIT编译log文件
jitwatch加载JIT编译log文件

这个界面中,顶部的工具栏都可以自己尝试一下,个人觉得“TopList”和“Suggest”比较直接,我们根据这两个就可以快速的定位需有优化哪些代码了,大体是什么原因导致未编译或者未内联。

选中方法后,点击“TriView”即可查看该方法和字节码和编译后的汇编代码,如下图:

jitwatch方法字节码和编译后的本地代码查看
jitwatch方法字节码和编译后的本地代码查看

如果你左边的java代码看不到,那你就需要在上一个界面中点击“Config”来添加源码路径或者源码文件以告诉JITWatch从哪里找源码;如果你右边的汇编代码看不到,说明你上面的hsdis未安装好,请重新安装。

此时,点击上面的“Chain”按钮,即可看到该方法调用了哪些方法,以及这些方法是否被编译了,是否被内联了。如下图所示:

JITWatch查看方法编译和内联状态
JITWatch查看方法编译和内联状态

总结

JIT的功能能显著提升java程序的性能,尤其是编译为本地代码和内联功能。内联需要方法比较小,也就是说写代码时就尽量把方法写得更小,让方法的复用度更高,复用的越多,就越可能被编译为本地代码。高性能的框架和类库针对JVM的JIT功能进行优化是非常有必要的,JVM提供的调试输出参数和JITWatch这样友好的工具能大大帮助我们快速的发现和定位需要优化的代码,大大提升了效率。

尽管我们可以手动调整JIT相关的一些参数,来让我们的更多的方法被编译和被内联,但一般不建议这么做(大牛都这么说)。

JIT编译成本地代码的过程也是需要消耗时间的,而且编译后本地代码不一定会使用(made not entrant,如果JVM根据一段时间的执行后进行了某项优化,但是在后来的某次执行时验证之前的优化是不完备的,那么JVM会取消这个优化,继续用解释执行的方式来执行字节码),所以并不是把所有或者大部分代码都编译一定会性能最优,那有可能也是灾难。

我所了解的JVM JIT性能调优的大致原理和方法就是这些,如有错误请指出。

性能优化永远是最后一步,不要提前过早开始性能优化。

Reference

如果你有耐心,就看看下面的文章吧,因为它们比我写的更详细

http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html

https://www.chrisnewland.com/images/jitwatch/HotSpot_Profiling_Using_JITWatch.pdf

http://www.docklandsljc.uk/presentations/2015/ChrisNewland-JITWatch.pdf

http://blog.csdn.net/hengyunabc/article/details/26898657

https://advancedweb.hu/2016/05/27/jvm_jit_optimization_techniques/

JVM启动参数:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

JITWatch使用文档:https://github.com/AdoptOpenJDK/jitwatch/wiki (右侧的页面目录分类很清晰)

https://advancedweb.hu/2016/05/27/jvm_jit_optimization_techniques/

https://advancedweb.hu/2016/06/28/jvm_jit_optimization_techniques_part_2/

sun.misc.Unsafe

Java是个安全的编程语言,它可以防止程序员犯一些低级错误,往往这些错误都是基于内存管理的。但是在Java中仍然有办法故意的犯这些错误——使用Unsafe类。 这篇文章将快速的阐述sun.misc.Unsafe类的public API和少许有意思的用法。

Unsafe类的初始化

在使用前,我们都需要创建一个Unsafe类的实例对象,但不能通过Unsafe unsafe = new Unsafe()方式来实现, 因为Unsafe类的构造函数是private的。Unsafe类有一个getUnsafe()的静态方法,但你如果尝试调用Unsafe.getUnsafe()的话,可能会抛出SecurityException异常,因为只有信任的代码才能使用这个静态方法。

上面的代码说明了java是如何校验代码是否是可信任的。它仅校验了我们的代码是否被BootClassloader加载。 要让我们都代码被信任,可以在运行你的程序时使用bootclasspath参数来指定系统类和你要使用Unsafe的类路径。

但是这个方法太麻烦了。 Unsafe类里面有个属性就是一个它的实例对象,即theUnsafe,这个属性是private的。我们可以通过反射方式把这个属性取出来。

注意:忽略你的IDE的error提示。比如:eclipse会提示“Access restriction…”,但你要是运行代码,会发现一切正常。如果你觉得这个提示很烦,可以这样设置来忽略这个提示:

 Unsafe API

sun.misc.Unsafe有105个方法,可以分为多组重要的操作对象属性的方法,这里列举一些:

  • Info. 返回一些底层的内存信息.
    • addressSize
    • pageSize
  • Objects. 提供了操作对象和其属性的方法
    • allocateInstance
    • objectFieldOffset
  • Classes. 提供了操作类和静态属性的方法
    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • Arrays. 数组操作
    • arrayBaseOffset
    • arrayIndexScale
  • Synchronization. 底层原始的同步管理
    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • Memory. 直接内存访问的方法
    • allocateMemory
    • copyMemory
    • freeMemory
    • getAddress
    • getInt
    • putInt

一些有意思的案例

避免实例化

allocateInstance方法在这几个场景下很有用:跳过对象的实例化阶段(通过构造函数)、忽略构造函数的安全检查(反射newInstance()时)、你需要某类的实例但该类没有public的构造函数。比如下面的类:

通过构造函数、反射方式创建实例与unsafe方式创建实例得到的效果不同:

由此想想你的单例真的能保证单例吗。

内存变更

这对C语言开发者很常用,他同时也是一种常见的跳过安全检查的方法,举个校验访问权限的例子:

客户端代码通过调用giveAccess()来校验访问权限,这种方式是很安全。不幸的是,不论谁调用这个方法,它都返回false。只有通过特殊手段来改变ACCESS_ALLOWED常量的值,从而使客户端获得访问权限。 下面是实例代码:

现在所有的客户端都可以获得无限制的访问权限。 实际上,我们可以通过反射的方式达到相同的效果,但有意思的是,使用unsafe可以让我们修改任意对象,甚至我们没有获取到对象的引用。 比如:在内存中有另外一个Guard对象在当前这个guard对象的下一个内存区块中,我们可以用下面的方式来修改它的ACCESS_ALLOWED属性:

注意,我们并没有这个新的Guard对象的引用,16是Guard对象在32位机器中占用的空间大小。我们可以通过马上要讲到的sizeOf方法来获取对象占用内存的大小。

sizeOf

使用obectFieldOffset方法可以使我们实现C语言的sizeOf功能。下面的实现就可以返回对象占用内存的大小(不包含子对象占用内存大小,暂且把这种内存大小称作“浅内存大小”):

算法是这样的:遍历所有当前类和所有父类的非静态属性,获得每个属性的偏移量,找到最大的偏移量,加上字节对齐所需的内存大小。可能不一定全,但思路是清晰的。 有个更简单的方式实现sizeOf,如果我们仅根据类的结构来读取对象占用的内存大小(该对象在JVM 1.7,32位机器上)分配的偏移量为12:

为了违反内存地址,normalize方法用来把int转成无符号的long:

很巧,这个方法返回的结果和前面的sizeof函数是相同的。 当然,要用更好更安全更精确的sizeof方法,最好使用java.lang.instrument包,但它需要在JVM上挂一个agent。

浅拷贝

既然已经实现了对象浅内存大小的计算功能,我们可以很容易的添加一点功能实现对象拷贝。标准的做法是让你的对象类实现Cloneable接口,或者在你的对象中自定义拷贝功能,但它都不具有通用性。 浅拷贝:

toAddress把对象转换成内存地址,而fromAddress则正好相反:

这种拷贝的方式可以用于任意类型的对象,对象的大小会自动的计算。值得注意的是,在拷贝以后,你需要把返回的对象强制转换为你需要的类型。

隐藏密码

一个更有意思的使用Unsafe直接访问内存的用法是:从内存中移除不想要的对象。 大部分获取用户密码的API都使用byte[]或者char[]类型,为什么是数组类型? 其实原因主要是为了安全,因为我们可以在我们不需要他们时把数组元素置为null。如果我们以String形式获取密码,他将以对象形式存储在内存中,这时把这个对象置为null相当于是使该对象解除引用。但这个对象仍然在内存中,除非它被GC回收掉。 下面的这个坑爹的例子用相同内存大小的String对象替换内存中的原始对象:

这样做,密码就安全了吗? 注意:这个方式仍不安全。因为安全的做法是需要通过反射的方式把String中的char数组中的每个元素置为”?”:

多重继承

要知道,在java中是不能多重继承的。 但是如果我们可以把任意类型转换为其他任意类型时,就另当别论了,比如下面的例子:

这个代码片段给String类添加了Integer作为其父类,所以我们可以做下面的类型转换,而不抛出运行时 异常:

这里为了欺骗编译器,需要先把Integer对象转换为Object类型。

动态类

我可以在运行时创建类,比如根据编译后得到.class文件来创建类。其机制是把.class的内容读取到byte数组中,并传给defineClass方法。

从文件中读取到byte数组中:

当你必须动态的创建类时,这个很管用,比如:一些代理或者基于已有的代码进行面向切面的编程。

抛异常

你是否不喜欢catch异常?没问题,这么干吧:

这个方法抛出一个需要被主动catch的异常,但是你的代码可以不去catch或者重新抛出异常,它就行抛出了一个运行时异常一样。(把IOException()当做运行时一样抛出,但调用方不需要catch)

快速的序列化

这个更有用些。 每个人知道java标准的Serializable功能性能很差,它还需要你的类必须要有一个无参的构造函数。 Externalizable 要好一点,但他需要为类的序列化过程进行定义。 热门高性能的库,例如kryo有依赖,这可能不适合低内存的场景。 但是完整的序列化和反序列过程可以很容易用unsafe来实现。

序列化:

  • 通过反射方式构建对象的schema,只需构架你一次就行了
  • 使用Unsafe的getLong、getInt、getObject等方法来获取实际的属性值
  • 添加一个类用来存储这个对象
  • 把对象写入到文件或者其他输出中

你也可以添加压缩功能来减少空间消耗。

反序列化:

  • 创建一个序列化类的实例。这时候allocateInstance方法就很有用了,因为它不需要任何构造函数
  • 构建schema,和序列化中的步骤一样
  • 从文件或者其他输入中读取所有的属性
  • 使用Unsafe的putLong、putInt、putObject方法等来填充前面创建的对象实例

实际上,有很多细节没有一一展开,但这个思路就是这样的。这个序列化反序列确实很快。顺便说一下,在kryo中有一些使用Unsafe的尝试:http://code.google.com/p/kryo/issues/detail?id=75

大数组

你可能知道Integer.MAX_VALUE是java数组的最大长度。通过直接的内存配分,我们可以创建出很大的java数组,其上限是堆空间的大小。

下面是SuperArray的实现:

可以这样使用SuperArray:

事实上,这种方式使用的是堆外内存空间(off heap memory),在java.nio包中有部分使用。

它并不是分配在java堆中,也不受java gc的管理,要小心使用Unsafe.freeMemory(),因为它不做任何边界检查,任何非法访问都会导致jvm crash。

它在数学计算中还是有用的,可以通过代码操作大数组数据。它对于实时系统开发的程序员来说很有意思,可以打破大数组gc上的限制。

并发

UnsafecompareAndSwap的并发简单来说就是,它说原子操作,可用于实现高并发的无锁数据结构。

例如,在多个线程间共享自增数据的问题:

首先,我们定义简单的接口定义:

然后,我们定义工作线程CounterClient,它使用到了Counter:

测试代码如下:

首先实现一个非同步的counter:

输出为:

运行很快,但是完全没有线程管理,所以结果说不准确的。

下一步,实现一个最简单的java同步counter:

输出为:

基础的同步功能还是起到了作用,但是耗时太长了。

下面让我们尝试使用ReentrantReadWriteLock:

出为:

仍然说正确的,耗时也好一些。如果使用原子操作会怎么样:

输出为:

AtomicCounter效果要更好一些。

最后,看看Unsafe原始的compareAndSwapLong方法是否真那么神奇:

输出为:

嗯,好像跟原子操作差不多,难道原子操作就是用这个这个方式?(答案:是)

实际上,这个例子很简单,但说明了Unsafe的强大之处。

如我所说,原始的CAS(CompareAndSwap)操作可以实现无锁的数据结构,其背后的机制是:

  • 有一定的状态
  • 拷贝一份
  • 修改它
  • 执行CAS
  • 如果执行CAS失败了,则这个过程

事实上,CAS比你想象的复杂得多,因为有很多问题,比如:ABA问题,指令重拍问题等。

如果你真得很感兴趣,可以看看这个牛x的ppt:无锁HashMap

注意:在counter变量上增加volatile,可以避免出现无限循环的风险。

彩蛋

Unsafe文档中park方法的注释有个我见过的最长的语句(。。。我也翻译不了了):

Block current thread, returning when a balancing unpark occurs, or a balancing unpark has already occurred, or the thread is interrupted, or, if not absolute and time is not zero, the given time nanoseconds have elapsed, or if absolute, the given deadline in milliseconds since Epoch has passed, or spuriously (i.e., returning for no “reason”). Note: This operation is in the Unsafe class only because unpark is, so it would be strange to place it elsewhere.

结论

虽然,Unsafe有一堆好用的用法,但永远不要使用它。

Reference

翻译自:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

 

 

 

 

java中坑爹的getter、setter方法的潜规则

众所周知的java中都会给类的属性写getter和setter方法,以getter方法为例,通常是get+属性的大写首字母+属性剩下的字符组成。比如:有个属性名为name,那么其getter方法名就是getName,如果name是boolean类型,那么getter方法就是isName,当然现在的getter和setter方法都不会自己手写了,一般是通过eclipse或者Intellij idea生成。

javaBean规范

javabean规范文档:http://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/,里面的有以下两个章节降到了具体的命名规则:

  • 8.3.1 Simple properties
  • 8.3.2 Boolean properties
  • 8.8 Capitalization of inferred names.

这个文档里面说明了,从getter和setter方法名如何推倒出propertyName:

  • 一般情况,把除去get或者is(如果是boolean类型)后的部分首字母转成小写即可,比如:getFoo –> foo
  • 如果除去get和is后端的部分,首字母和第二个字母都是大写,不作转换即可,比如:getXPath –> XPath

意想不到的getter和setter生成规则

现在大家都使用ide来帮忙生成getter和setter,那么ide的规则就算是我们的标准了吧,是否把javabean标准反过来推理就行了呢?经测试实际上还有一些场景可能都想不到,比如:首字母不用大写;多个不同的属性(字母大小写不同)的getter方法是相同的。看下面的例子(用eclipse生成getter测试):

看到这里,你是否有种隐隐的蛋疼感~~

什么时候你需要关注getter和setter方法的生成规则?

  1. 想要序列化为json对象时,如果你使用gson的话,基本没啥问题,但要是你使用了低版本的fastjson,那么可能会中枪了
  2. 要自己写代码拼装出getter和setter方法名,以此来通过反射查找Method时

个人建议

  1. 属性的前两个都以小写开头
  2. boolean类型不要用is开头

【译】用maven使java web应用运行在内嵌的Jetty或Tomcat容器中

apache maven
apache maven

开发java web应用时,能在“真实”的环境中有快速的反馈是非常实用的。本文将探寻如何使用maven使java web应用运行在内嵌的Jetty或Tomcat容器中。我将展示如何配置他们来开发Podcastpedia.org网站的工程podcastpedia。

环境准备

你需要有Maven,至少安装了Java 7。正常情况你应该可以自行部署和启动podcastpedia工程并看到效果

Jetty Maven Plugin

插件配置

注意:

  • jettyConfig指定Jetty的配置文件,下一部分将有该配置文件的具体内容
  • scanTargets指定了Jetty监控文件变化的文件夹
  • 指定连接数据库和发邮件的依赖包

Jetty.xml配置文件

Jetty配置文件中,你需要配置一下内容:

  • Server类(或者子类)以及全局的可选项
  • 一个线程池(最小、最大线程数)
  • Connectors连接器(端口号、超时时间、缓冲区大小、协议)
  • 处理器结构(handler structure)(默认的处理器或者一个contextHandlerCollections)
  • 扫描部署的webapps和容器上下文的部署管理器
  • 提供认证的登录服务
  • 请求日志

Apache Tomcat Maven插件

apache tomcat maven插件的配置

注意:

  • 指定tomcat的端口号
  • 指定contextFile,用来告诉tomcat配置文件在哪里
  • 指定连接数据库和发邮件的依赖包

context.xml

在context.xml中,定义了数据库和邮件资源

就这样,使用了spring框架的Java web应用可以运行轻量级的servlet容器,显然这种方式可以代替JavaEE服务器及其带来的所有成本。

参考

  1. Jetty Maven Plugin
  2. Apache Tomcat Maven Plugin

翻译:http://www.javacodegeeks.com/2015/06/run-java-web-apps-in-embedded-containers-with-maven-jetty-and-tomcat.html

 

 

【译】怎样开发一个高度可定制的产品

你可曾听过:“我真的很喜欢你的产品,除了一些小的细节点”?然后CIO(首席信息官)推出一个必须有的需求清单,这清单中的数百个需求点需要添加到你的了不起的产品中。你可曾听过或者甚至说过:“同志们,我们将签署一个高利润的合同,但是。。。”?然后客户期望的额外的功能变成了开发工程师的头痛点。

所以,尽管现在你的产品能满足顾客的需求,但怎样才能让你的产品远离顾客潜在的有风险的想法?怎样才能在已有无数加载项的情况下,维系一个产品技术设计的最高水平,来让其在特定的方式下起作用?这些为成熟的解决方案提供可靠且突出支持的基础性需求,将面临多少挑战?

在商业世界,产品定制是一种越来越可取的要求和一些共通的做法,这些做法已经演变为响应这一客户需求。下面你将找到一个大致的方法,如果你已经对它们比较了解了,你可以直接拖到到“方法五:扩展法”章节,来了解我们怎样以一种我们自认为有效的方式克服这些挑战。

方法一:在一个产品中包含所有定制功能(All in One)

最直接最容易想到的定制化的办法就是在一个产品核心中实现所有的需求,然后通过“属性开关”的技术来匹配不同客户的需求。

这种方法的主要优点是始终保持一个单一产品,对于不需要太多扩展就能覆盖大多数业务需求的场景是比较管用的。

但这种方法其实隐藏了一个假设,即“没有太多了定制需求”。通常,产品的开发都以这个信念开发,但在几伦迭代交付之后,你会看到客户真正的定制需求量。陷入一个左右为难的困境并不罕见:拒绝定制或者潜在地丢失客户,或者把代码变成了一个“垃圾桶”,因为为一个客户定制太多的个性化需求对其他主要客户都是无用的。

你会选择哪种?很显然,从一个岩石和一个坚硬的东西中选择一个都不会成功。

总结:在一个产品中包含所有定制功能,只在定制需求很少或者有限时才算是合理的选择。否则,你将在可维护性和客户满意度之间做出选择。在此引用Jerry Garcia的话:“从两个不太坏的恶魔中选择一个,不还是恶魔吗”。

方法二:分支开发(Branching)

如果重要的定制是交付中必须要有的部分,那么All in One的技术方案就行不通了。有另外的更直接了当的方法——分支开发。你可以很容易的创建一个独立的产品分支,随意修改。

与All in One相比,分支开发最大的优势是没有定制范围的限制,通过分支的方式来分隔不同客户的特定需求,避免了在同一套代码中混杂所有的功能。

但是,这个优势的另一面可能让产品演化走向死路。显而易见的,产品分支是主要的开发领域:大部分的bugfix、改进、新功能首先添加到产品中,那么,要保持定制的分支和主产品保持同步,就需要频繁的合并代码,合并代码时要是和主干产品没有冲突,那合并代码是很容易的操作;要是有冲突,合并操作将非常耗时,且不可避免的需要回归测试。

如果定制分支很少时,这种分支开发的方式是可行的,但是当交付的产品数量增加以后,面临痛苦合并的可能性将迫在眉睫。

总结:分支开发无疑是非常灵活和直接的方法——产品的任意部分都可以修改。但是在交付的后期将很费力,随着时间的推移,将变得更困难,也不太可能有太多的定制分支。

方法三:实体-属性-值模型(EAV)

实体-属性-值 模型(Entity-Attribute-Value model又名:Object-Attribute-Value model,垂直数据库模型和开放schema)是一种熟知的广泛使用的数据模型。EAV可以支持动态的实体属性,它适合用于平行的标准关系模型。

从产品角度来看EAV的优势是:你可以交付你的产品“as is”,然后通过在运行时添加需要的属性来调整数据模型,而不需要修改源码。

还是一样的有点不足:

  • 适用性有限:该模型仅适用于这样的场景,基于已经编写好的逻辑,使用添加属性的方式,使其自动的嵌入到界面中
  • 额外的DB服务器负担:垂直数据库的设计通常会变成企业级应用的瓶颈,因为它通常操作大量的与应用相关的实体和属性。
  • 最后,企业级系统不可能没有复杂的报表引擎。EAV模型将因为它的“垂直”数据库结构带来许多潜在的并发症。

总结:EAV模型在某些场景下很有有用,比如:提供一种附加信息数据即可获得灵活性的需求,这种附加的信息数据并不显示地在业务逻辑中使用。换句话说,EAV适度是好的,除了标准的关系模型和插件架构。

方法四:插件架构(The Plugin Architecture)

插件架构是最常用最强大的一种方式之一:功能逻辑分别放在名为插件的东西里面。要想覆盖现有的盒子外的行为,并运行插件,就必须在产品的源码中定义“定制点”(又名扩展点)。一个“定制点”是某些源码中的片段,应用会扫描所有的插件,来检查是否有插件重写了指定的实现,如果实现了就执行该插件;另一种插件架构方式是外部脚本,功能实现被实现和存储在外部的脚本中,脚本的调用被预定义的“定制点”控制。

使用插件方式可以保持产品代码的纯净,可以不做修改的交付核心产品,而把定制的行为放在插件或者脚本中。另一个优点是可以很好的管理更新的过程。产品和插件的完全分离使他们可以相互独立的升级。

当然,不足之处在于:原则上,你不可能知道客户在未来提出的需求。因此,只可能猜测“定制点”要嵌在哪里。当然,你可以把“定制点”分散在所有可能的地方以缓解“万一需要的场景”,但这将导致代码可读性很差,代码很难调试,增添了复杂性。

总结:插件模式确实在“定制点”可预测的场景下工作,但要注意的是:在“定制点”间的定制是不可能的。

方法五:扩展法(The Extensions Approach)

在我们的企业级软件开发平台CUBA中,我们已经实现了一个独特的方法。正如我们之前的文章,CUBA是非常实用的,在一套开发驱动的演化中创造出来的活的有机体。所以,基于我们丰富的已有产品的经验,我们提出了两个终极需求:

  • 为客户定制的代码必须和核心产品代码完全分离
  • 每个产品的代码部分必须可以被修改

我们设法满足这些需求,甚至从这种扩展机制中获得了更多好处。

CUBA的扩展

一个扩展就是一个独立的CUBA工程,它把底层工程当作依赖库来使用,继承了所有底层工程的特性(比如:你的核心产品),这很显然允许开发者实现新功能,而不影响父工程,但得益于开放继承模式和专门的CUBA设施,你也可以重写任何父工程的部分功能。总之,一个扩展就是你可以实现成千上万个文章开头提到的“小细节点”的地方。

事实上,每个CUBA工程是一个CUBA平台自身的扩展,所以它能重写任意的平台功能。我们自己采用这种方式来区分出核心平台之外的功能。所以如果在你的工程中需要他们,你只需要把他们当作父工程添加到你的工程中即可,类似多重继承!

用相同的方法,你可以构建一个可继承的自定义模型。这可能听起来比较复杂,但它很管用。让我给个真实的例子:SherlockHaulmont’s的完备的出租车管理方案,支持每个运行出租车业务的各个方面,从预定和分派到应用和结算。这个方案涵盖了需要不同的客户业务,并且这些业务中很多是和本地化相关的。比如:所有英国的出租车公司有相同的法规,但有的法规在美国却行不通,反之亦然。很显然,我们不想在核心产品中实现所有这些法规,因为:

  • 这是一个“操作层面的具体的”功能
  • 在不同的国家,本地化的法规可能对出租车队管理的影响完全不同
  • 有的客户根本不需要法规

所以,我们组织一下多层扩展:

  1. 核心产品包含通用出租车业务功能
  2. 第一层定制实现区域特殊化
  3. 第二层定制覆盖客户的需求清单(如果有的话!)
cuba-platform_product-schema
CUBA的扩展架构图

非常干净。

如你所见,使用扩展的方式,保持了代码的干净和可维护性,你既不需要以分支方式开发你的产品,也不需要迁移所有的需求到你的核心产品中。看起来真的很好,所以让我们看看它在代码实践中如何工作!

给已有的实体添加新的属性

让我们假设我们有一个User实体定义,它由两个属性组成:login和password:

现在我们的客户有个额外的需求,要添加一个“home address”属性到User实体中,要做到这个,我们在扩展中继承User实体:

你可能已经发现,所有的注解,除了@Extend,都是JPA的注解。@Extend注解是CUBA引擎的一部分,它会用ExtUser全局替换User实体,甚至跨产品功能。

使用@Extend注解,我们必须让平台:

  1. 总是创建继承结构中叶子类型的实体
  2. 在执行前,转换所有JPQL的查询语句,以便我们总是返回叶子类型的集合
  3. 在关联的实体中总是使用叶子类型

换句话说,如果声明了一个继承的实体,父实体就在整个产品和扩展中被遗弃,并且被继承的实体全局替换。

界面定制

我们已经通过添加address属性来继承了User实体,现在我们想要让这个继承效果作用到用户界面中。首先,让我们看看原始界面是什么样子:

可以看到,一个CUBA的界面描述是以XML的方式呈现。显然,我们可以简单的在扩展中重新声明整个界面描述,但是这意味着复制-粘贴大部分的描述代码,它导致如果以后产品界面有点变化,我们就需要手动地把这些变化点复制到扩展的产品界面中。为了避免这个,CUBA引入了界面继承机制,你所需要做的仅是描述不同的界面部分:

使用extends属性来定义祖先界面描述,然后只需要描述扩展需求中不同的部分即可。

走你,让我们最后看看界面效果

cuba-platform_admin
CUBA扩展后的界面效果

业务逻辑修改

为了修改CUBA平台的业务逻辑,CUBA平台使用了Spring框架作为其平台基础的核心部分。

比如:你有一个中间件来执行价格计算:

要重写价格计算的实现,我们仅需要两个步骤:

第一步:继承产品类,并重写相应的方法:

第二部:在spring中配置这个产品bean:

现在PriceCalculator注入将总是返回继承的类的实例,这样,继承的实现将作用到整个产品中。

在扩展中升级基础产品的版本

随着核心产品的演进和新版本的发布,你终究会决定把你的扩展升级到最新的产品版本。这个过程非常简单:

  1. 在扩展中指定底层产品的新版本
  2. 重新构建扩展:
    1. 如果扩展扩展是构建在稳定的产品API基础上,那么它应该可以跑起来了
    2. 如果产品API有些重要的修改发生,且这些修改与定制的实现有冲突,那就要在扩展中支持新的API

大多数时候,产品API不会再更新中有太多的变更,尤其是小版本的发布。但是就算API的大改发生了,一个产品通常也会在未来的几个版本中保持向下兼容,并在老的实现中标记“deprecated”,允许所有的扩展迁移到新的API上。

总结

简单的汇总一下,我比较喜欢用对比分析的表格形式来阐述:

/ All in One Branching EAV Plugins CUBA Extensions
Architecture independent + +
Dynamic customization + +/-
Business logic customization + + +/- +
Data model customization + + + +/- +
UI customization + + +/- +/- +
Code quality and readability +/- +/- +/- +
Doesn’t affect performance + + + +
Risk of software regression High High Low Medium Medium
The complexity of long term support Extremal Extremal Low Medium Medium
Scalability Low Low High High High

 

如你所见,扩展方式是很强大的,但它有一条不足,就是在运行中动态微调的能力不够。为了克服这个问题,CUBA也提供了全面的EAV模型和插件/脚本方式的支持。

翻译自:http://www.javacodegeeks.com/2015/07/how-to-develop-a-highly-customizable-product.html

【译】使用 Lombok 减少你的样板代码

Lomok是一个用来简化java代码开发的java库。这篇文章主要解决getters/setters、重写equals、hashCode、toString以及构造函数的编写问题。如果用普通的java编写方式,一定很繁琐。当你看到Lombok项目时,它会让你觉得一身轻松。

使用 Lombok 减少你的样板代码

1. 不用再写getters\setters方法了

比如下面的pojo类,用普通的方式写会是这样:

当写这些getters/setters方法时,我开始慢慢讨厌java了。

使用Lomok的方式来写会是这样:

是不是很容易?

除了这些,Lombok API还提供了其他很棒的特性:

  1. 不用重写toString方法:可以在pojo类上使用@ToString注解,lombok会把重写后的toString方法放到类中
  2. 不用重写equals和hashCode方法:在pojo类上使用@EqualsAndHashCode注解,在编译的时候很容易生成equal射hashCode方法
  3. 用注解来生成构造函数:
    • @NoArgsConstructor:用来创建无参的构造函数
    • @RequiredArgsConstructor:用来创建构造函数,其参数由所有未初始化的final字段和标记了@NonNull注解的字段
    • @AllArgsConstructor:用所有的字段作为参数,来创建构造函数

这些都是很常用的特性,其他特性可以在这里找到:project lombok site

使用了Lombok和未使用Lombok对比的例子

比如我们需要一个类,这个类需要包含equals、hashCode、toString、getters\setters、构造函数。

如果用Lombok来写:

这样写后Lombok会把这段代码转成常规的java pojo类代码

如果不用Lombok来写:

此时可能很多java开发会争辩了:“IDE可以帮我们生成这些繁琐的代码,速度比写注解来得更快把!”

如果你这样想,那么你可能遗漏了一点:人们不喜欢java,因为控制和编写所有这些代码其实是一种坏味道。相同的代码在Ruby、Groovy、Perl或者其他脚本语言中都很简单,为啥在java中如此冗长呢。简单才是好,Lombok正是为了java的简单而存在。(java开发应该把更多的时间放在其他的业务逻辑中,不应该在这些地方浪费时间)。

详细内容请看Lombok的官网:https://projectlombok.org/,里面有一段视频介绍,能很清晰介绍如何在eclipse中使用Lombok来简化你的开发。

Lombok的在eclipse中的安装和使用

  • 官网上有视频:https://projectlombok.org/。
  • 安装和使用的文档:http://jnb.ociweb.com/jnb/jnbJan2010.html
  • Lombok其实是通过agent机制在eclipse的启动参数中添加了一个agent(-javaagent:lombok.jar)

Lombok在maven中的使用

在maven中依赖上Lombok:https://projectlombok.org/mavenrepo/index.html

如果需要在maven中使用其打包,请看这里:http://awhitford.github.io/lombok.maven/lombok-maven-plugin/usage.html

Reference

  • 原文:http://keaplogik.blogspot.com/2013/04/java-developers-need-to-be-using-lombok.html
  • http://www.oschina.net/translate/lombok-reduces-your-boilerplate-code?cmp
  • http://www.ibm.com/developerworks/cn/java/j-lombok/

一点数据库访问优化的思考

最近碰到了老是数据库连接池爆满的情况,于是调研了下应用中的代码存在哪些问题,哪些地方可以优化,于是又了下面的一点思路,欢迎抛砖。

核心思路是:减少单页面请求数据库的sql数量,缩短单url请求数据库的时间。

平时开发时养成性能优化的思维模式能大大减少后期性能优化的成本,因为开发过程中,很多业务和实现细节了然于胸,如果此时就考虑性能优化,就能始终保持最大化整个应用的性能(当然前提是,如果你没那么急的话)。

数据库访问优化
数据库访问优化

 

 

 

fastjson解析大文件时抛出java.lang.ArrayIndexOutOfBoundsException: -1

最近发现偶尔出现fastjson解析大文件时抛出java.lang.ArrayIndexOutOfBoundsException: -1异常。可以我是按照官方的中的实例代码来写的,如下的代码:

所以非常不解,我自己单独写代码解析json文件又没有问题,在应用跑了一段时间后,就突然出现这个问题。由于fastjson的代码没有深入阅读,而且异常还不是每次都能碰到,因此没有查到直接原因。但同事发现换个api来解析就不抛出异常了,因此在这里记录下,下次朋友类似的情况,也可以按照种方式来解决。代码这样改:

可能这种改法效率没有上面那个好,但是这个确实没有抛出异常了,至少在可用性方面得到保证。如果你也碰到这种情况,也可以尝试这样修改你代码。