开发小记(一)

从”零”开始已经一个月了,总结一下自己在这段时间学到的东西吧。 虽然很基础,但是总要有一个慢慢积累的过程。

Java8流处理语法

这里简单介绍几个我用过的语法。

1
2
3
4
5
6
7
8
9
10
// 取出entity中的某个属性
List<String> studentNames = list.stream().map(student -> student.getName()).collect(Collectors.toList());

// 取出符合条件的实体
List<Student> students = list.stream().filter(student -> student.getAge() >= 18).collect(Collectors.toList());

// 更简洁的方式遍历集合(注意,若list为null,会抛出NPE)
list.forEach(student -> {
//do something
});

Optional语法

Optional 类主要解决的问题是空指针异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//根据是否为空,决定函数返回值
public static String getGender(Student student) {
if (null == student) {
return "Unkown";
}
return student.getGender();
}
//使用Optional
public static String getGender(Student student) {
return Optional.ofNullable(student).map(u -> u.getGender()).orElse("Unkown");
}

//Optional中orElse的源码
public T orElse(T other) {
return value != null ? value : other;
}
1
2
3
4
//使用ifPresent避免判空
public static void printName(Student student){
Optional.ofNullable(student).ifPresent(u -> System.out.println("The student name is : " + u.getName()));
}

使用Optional开发时要注意正确使用Optional的“姿势”,谨慎使用isPresent()和get()方法,尽量多使用map()、filter()、orElse()等方法来发挥Optional的作用。

同事写AOP时遇到的一个小问题

在某个需求中,需要对一些接口中的uid进行鉴权。由于不同的方法可能对应不同的权限码,所以实现如下:

在实现了某个接口的类中需要鉴权的方法上,添加了注解 @AuthType(AuthCode = AuthEnum.UserType)

在AOP处理的类中取该注解,但是遇到一个问题,取不到该注解。

这里就需要谈谈AOP的实现原理了,众所周知,AOP是通过动态代理的方式来实现的,有JDK动态代理和cglib两种方式。

  • JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。
  • CGLIB动态代理,可以在运行时动态的生成某个类的子类。通过字节码技术生成,创建速度慢,执行不需要反射。生成的类会在Java的永久代中。

看到这里可能已经有人明白了,Spring AOP默认使用的是JDK动态代理的方式,在实现类中加的注解,在切面中自然取不到了。

解决方案:当我们需要强制使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true@EnableAspectJAutoProxy(proxyTargetClass = true)

为什么插件自动给变量加final

最近在原来的类中编写类的代码的时候,发现由于IDEA某个插件,会自动给有初始值且并未修改过的全局变量加final。

由此就考虑到一个问题,这样加final的好处是什么呢?我们来看看两者的区别。

这里可以使用IDEA的【ByteCode Viewer】插件来查看类的字节码。

1
2
3
4
5
//源代码如下
public class Test2 {
//此处为不加final的版本
private static int num = 6;
}

这里是加final和不加final关键字的两个类的字节码的区别。

微信截图_20200627010744.png

这里看到有一段static <clinit>的指令,即类加载初始化过程中的 (clinit) 方法,也就是静态变量赋值以及静态代码块中的代码。

这里就需要提一下类加载过程了。众所周知,类加载过程分为加载、验证、准备、解析、初始化这五步。

  1. 未加final关键字时。在准备阶段为类变量(static修饰的)分配内存并设置类变量初始值(值数据类型的零值,对于int来说是0)。在初始化阶段,执行类构造器<clinit>方法(由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生)

    虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>方法完毕。

  2. 加了final关键字后。在准备阶段,虚拟机就会将num赋值为6.

这里还需要提一个事情,就是类加载的时机。《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。 但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行初始化,则需要先触发其初始化阶段。典型场景:使用new关键字实例化对象时、读取或设置一个类型的静态字段、调用一个类型的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候。
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要自定一个要执行的主类,虚拟机会先初始化这个主类。
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

所以这里就大概知道结论了。

  • 对于Spring管理的类,需要在JVM启动时,实例化对象并注入Spring容器。添加final关键字会使得服务启动相对来说更快。

  • 对于都是静态方法的工具类,会在使用静态方法的时候,初始化类。此时JVM还会加锁,保证只会有一个线程去执行这个类的<clinit>方法。添加final关键字会避免这个过程,在并发情况下提高了性能。

参考