一个特殊字符与一下午

今天是这个迭代的最后一周。本来这个迭代工作交付还是比较顺利的,除了个别几个联调的需求,大家都在收尾解bug了。 我最后一个bug是一个多语问题

Java8Stream用法整理

平常经常在用Stream,但是很少全面的整理一下用法。今天是基于Java8版本对Stream的用法进行了整理。 Stream是Java8提供的

tcpdump排查现网问题两例

最近遇到了一件郁闷的事情,服务本身指标都正常,唯独搜索会超时,经过排查,非常明确是 ES 的 client 在发出 HTTP 请求以后,超时导致了用户结果异常。 本以为把问

浅谈Java开发中Redis的使用姿势

Redis是我们开发时最常使用的键值存储工具了,但是与诸多软件工具一样,它也有很多技巧和经验。本文就是将我了解的一些经验和教训分享出来。 使用

走近DDD

《领域驱动设计精粹》这本书是 DDD 的发明者Evans在提出DDD多年后写的一本小册子,是为了降低DDD上手难度而写的一本小册子,它很棒地阐述了D

提升个人影响力的简单原则

今天看到一篇讨论个人影响力的文章,感觉讲得不错,分享给大家。原文地址 个人成长和投资一样,要学会利用杠杆的力量,影响力就是一项可以帮助自己更高

走进Lombok

Lombok 是什么

Lombok 是为了解决 Java 语法啰嗦而产生的工具,能够自动为编辑器和编译器产生代码,避免一部分体力劳动。比如我有一个Pojo,本来它是这个样子的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Test {  
    public static void main(String[] args) {  
        final Pojo build = new Pojo();  
                build.setA("a");  
        build.setB(true);  
        build.setNames(Arrays.asList("name1","name2"));  
        System.out.println(build);  
    }  
  
    public static class Pojo {  
        String a;  
        Boolean b;  
        List<String> names;  
  
        public String getA() {  
            return a;  
        }  
  
        public void setA(final String a) {  
            this.a = a;  
        }  
  
        public Boolean getB() {  
            return b;  
        }  
  
        public void setB(final Boolean b) {  
            this.b = b;  
        }  
  
        public List<String> getNames() {  
            return names;  
        }  
  
        public void setNames(final List<String> names) {  
            this.names = names;  
        }  
        
        @Override  
        public String toString() {  
            return "Test.Pojo{" +  
                "a='" + a + '\'' +  
                ", b=" + b +  
                ", names=" + names +  
                '}';  
        }
    }  
}

但是我用了Lombok以后,它是这个样子的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {  
    public static void main(String[] args) {  
        final Pojo build = Pojo.builder()  
                .a("a")  
                .b(true)  
                .name("name1")  
                .name("name2")  
                .build();  
        System.out.println(build);  
    }  
  
    @Data  
    @Builder
    @NoArgsConstructor
    static class Pojo {  
        String a;  
        Boolean b;  
        @Singular  
        List<String> names;  
    }  
}

输出是Test.Pojo(a=a, b=true, names=[name1, name2])

是不是可读性高了不少?这就是Lombok的效果了。

Lombok 怎么用

Lombok 是通过注解来完成其功能的。我把Lombok的注释分为如下几类:

  1. Pojo代码生成类
  2. 语义增强类
  3. 变量的注入和局部变量增强类

Pojo代码生成类

这一类注解主要用于Pojo里,可以避免体力劳动。

@Getter @Setter

最常用的,莫过于Getter和Setter,正如名字所示,它们可以为类和字段生成getter和setter。默认可以通过AccessLevel控制生成的函数的公开程度。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Test {  
    public static void main(String[] args) {  
        final Pojo pojo = new Pojo();  
        pojo.setA("a");  
        pojo.setB(true);  
        pojo.setNames(Arrays.asList("name1","name2"));  
        System.out.println(pojo);  
    }  
  
    @Setter  
    @Getter 
    public static class Pojo {  
        String a;  
        Boolean b;  
        List<String> names;  
    }  
}

@NoArgsConstructor @AllArgsConstructor @RequiredArgsConstructor

这几个也很常用,分别是无参构造函数,所有参数的构造函数,以及必填参数的构造函数。无参构造函数和所有参数的构造函数都很好理解,最后这个必填参数怎么去找呢?是找到打了@NonNull注解,或者是final的字段,会当成是必填字段。

这三个构造函数的注解还有一个能力,可以通过staticName,来生成一个静态的工厂,此工厂可以根据参数来使用相应的构造函数。这个能力通常是用来接受一些类型参数来构造一个具有某种类型信息的Pojo。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Test {  
    public static void main(String[] args) {  
        final Pojo pojo = new Pojo(Arrays.asList("name1","name2"));  
        pojo.setA("a");  
        pojo.setB(true);  
//        pojo.setNames(Arrays.asList("name1","name2")); 不能再set  
//        pojo.getNames().add("name3"); java.lang.UnsupportedOperationException  
        System.out.println(pojo);  
    }  
  
    @Data  
    @AllArgsConstructor 
    @RequiredArgsConstructor 
    public static class Pojo {  
        String a;  
        Boolean b;  
        final List<String> names;  
    }  
}

@ToString @EqualsAndHashCode

这两个注解能够为被注解的类增加toString,equals和hashCode函数。如果有继承关系,一般还要把callSupper=true打开,可以将父类的字段囊括到计算中。如果对象里有一些字段是不用参与相等判断的,可以用exclude来排除一些字段。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Test {  
    public static void main(String[] args) {  
        final Pojo pojo = new Pojo();  
        pojo.setA("a");  
        pojo.setB(true);  
        pojo.setNames(Arrays.asList("name1","name2"));  
        System.out.println(pojo);  
    }  
  
    @ToString  
    @EqualsAndHashCode
    @Setter 
    @Getter
    public static class Pojo {  
        String a;  
        Boolean b;  
        List<String> names;  
    }  
}

@Data @Value

@Data其实是@Getter,@Setter,@RequiredArgConstructor,@ToString,@EqualsAndHashCode的集合,而@Value则是@Getter,@FieldDefault,@AllArgsConstructor,@ToString,@EqualsAndHashCode和@Data的集合。基本上打了这两个注解的,一定是当成了一个Pojo来使用。它们的区别是什么呢?@Data的对象没有什么特别的,但是@Value的对象,是当作不可变对象来使用的,一旦构造结束,也就不能变更了。但是要注意的是,上述@Data不包括@NoArgsConstrutor,当你的Pojo在经历反序列化过程的时候有可能会报错,需要人为补充@NoArgsConstructor。

@Builder @Singular

@Builder 可以为指定的Pojo生成一个Builder,通过Builder去实现一个类的Builder模式,解决构造的参数复杂异变带来的问题,如果说这个Builder还有哪里难用,那就是它默认对数组的设置需要填写一个完整的数组。众所周知,Java构造一个数组的语法是相当的啰嗦。而@Singular则可以为Builder中的数组元素提供单个元素的增加方法,build一个复杂对象更加容易了。开篇的示例就是这两个注解的使用方法。

@With

通过在字段上的注解With,我们可以通过With,将原有对象复制一份,所有字段都一样,除了with所指定的字段是新的。

语义增强类

@CustomLog

这是为了给被打注解的类注入一个logger。对于lombok.log.custom.declaration=my.cool.Logger my.cool.LoggerFactory.getLogger(NAME)的配置来说,会对目标类

1
2
3
@CustomLog  
public class LogExample {  
}

生成如下代码

1
private static final my.cool.Logger log = my.cool.LoggerFactory.getLogger(LogExample.class.getName());

但是现在通常使用已经封装好的Logger,所以,它有@Log,@CommonsLog,@Log4j,@Log4j2,@Slf4j,@XSlf4j,@JBossLog,@Flogger一系列注解,可以直接使用项目里已有的logger。

@SneakyThrows

Java面试题里经常问,怎么才能偷偷在一个没有声明Throws的函数上抛出一个checkedException?这个大家知道了原理,但是用的时候,大可不必自己造轮子,因为Lombok已经把轮子造好了,原理是利用了泛型抹除。但是我觉得这个注解应该慎用,因为用了这个注解,就把函数里的异常抛出情况掩盖了。比如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {  
    public static void main(String[] args) {  
        final Pojo pojo = new Pojo();  
        pojo.setA("a");  
        pojo.setB(true);  
        pojo.setNames(Arrays.asList("name1","name2"));  
        pojo.test();  
    }  
  
    @Data  
    public static class Pojo {  
        String a;  
        Boolean b;  
        List<String> names;  
          
        @SneakyThrows  
        public void test(){  
            throw new OperationNotSupportedException();  
        }  
    }  
}

会输出Exception in thread "main" javax.naming.OperationNotSupportedException

@Synchronized

这个注释名字大家太熟悉了,我们先看个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {  
    @SneakyThrows  
    public static void main(String[] args) {  
        final Pojo pojo = new Pojo();  
        pojo.setA("a");  
        pojo.setB(true);  
        pojo.setNames(Arrays.asList("name1","name2"));  
        int concurrency = 10;  
        final ExecutorService executorService = Executors.newFixedThreadPool(concurrency);  
        final CountDownLatch countDownLatch = new CountDownLatch(concurrency);  
        final List<Callable<Object>> collect = IntStream.range(0,concurrency).mapToObj(i -> new Callable<Object>() {  
            @Override  
            public Object call() throws Exception {  
                pojo.counterAdd(i);  
                countDownLatch.countDown();  
                return null;  
            }  
        }).collect(Collectors.toList());  
        final List<Future<Object>> futures = executorService.invokeAll(collect);  
        countDownLatch.await();  
        executorService.shutdown();  
        System.out.println(pojo.getCounter());  
    }  
  
    @Data  
    public static class Pojo {  
        String a;  
        Boolean b;  
        List<String> names;  
        volatile int counter = 0;  
  
        @Synchronized  
        public void counterAdd(final int i){  
            counter += i;  
        }  
    }  
}

由于counter的add是有并发的情况的,通过注解,保证调用时不会导致加法失效。

乍一看这个注释跟我直接在函数上加修饰符synchronized,难道不一样吗?嘿,别说,虽然例子里的效果一样,但是原理还就是不一样。在函数上加的修饰符,锁的是对象的实例;用Lombok的注解,加的锁是对象里生成的一个字段,默认名称lock,我们可以根据自己需要调整不同的名称,达到更细粒度的锁。而且这能防止this在对象外被别人拿去当锁了,会造成不可以意料的死锁。

变量的注入和局部变量增强类

@Cleanup

在try with语法之前,关闭closable通常都写得很罗嗦,尤其是有嵌套的时候——这种情况还挺常见。即便是现在有了try with语法,多层嵌套还是很难看。但是用了@Cleanup,那就简单多了。比如这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {  
    @SneakyThrows  
    public static void main(String[] args) {  
        @Cleanup final Pojo pojo1 = Pojo.builder().name("pojo1").build();  
        System.out.println(pojo1+ " created.");  
        @Cleanup final Pojo pojo2 = Pojo.builder().name("pojo2").build();  
        System.out.println(pojo2+ " created.");  
        System.out.println(pojo1);  
        System.out.println(pojo2);  
    }  
  
    @Data  
    @Builder 
    public static class Pojo implements Closeable {  
        String name;  
  
        @Override  
        public void close() throws IOException {  
            System.out.println(this + " closed.");  
        }  
    }  
}

最后输出是

1
2
3
4
5
6
Test.Pojo(name=pojo1) created.
Test.Pojo(name=pojo2) created.
Test.Pojo(name=pojo1)
Test.Pojo(name=pojo2)
Test.Pojo(name=pojo2) closed.
Test.Pojo(name=pojo1) closed.

我们看关闭时顺序与创建的顺序是反的,完全正确。

@val @var

在Java支持var 和 val之前,一个类的名字如果要尽可能准确,那长度就很难减少;但是Java语法要赋值一个变量,就得为这个变量指定类型,最终,一个赋值都能写一百多列,非常难看。用了这组注释,再也不用写超级长的类名了。正如大家常用的其它语言,val是不可变量,var是可变量。不过现在Java已经支持了val和var,这个注解就用不到了。

Lombok 的原理

说完了用法,我们肯定会好奇,它为什么引入了依赖,就能自动帮我们完成这么多复杂的工作?通常我们写注解,都会自己写一些注解解析工具来完成注解的的使用工作,而这种工具通常得明确定义出来,或者放到框架的某个生命周期才能生效。Lombok是怎么做到的呢?

原来Lombok使用了一个Java的特性:JSR 269,也就是Pluggable Annotation Processing API。这样在Java编译的时候,Lombok就会被Javac唤起,用来增强实现的效果。如果想了解其具体的实现方案,可以看lombok.javac.handlers包下的各种handler。

Lombok 的注意事项

构造函数的易变性

如果过度依赖Lombok提供的注解来生成构造函数的话,一定要留意对象变更时这几个构造函数的重叠问题。比如当对象里的字段都废弃掉的时候,无参构造函数和全参构造函数参数一致了,编译过程会报错;或者增减字段的时候,原先的全参构造函数被直接使用的地方编译就报错了,也得改代码。更不要说调整字段顺序的时候全参构造函数一定会失效。我认为最好使用builder进行构造,语义清晰,而且修改的时候不容易错。

默认构造函数的丢失问题 @AllArgsConstructor @Builder

如果一个Pojo,只打了@AllArgsConstructor或@Builder,那它会吃掉默认的无参构造函数,在一些序列化框架上会报错。所以这个通常都会把无参构造函数的注解打上去。

注解的继承问题

@Data默认是有@EqualsAndHashCode注解的,但是callSuper并没有打开,也就是说,两个打了@Data的子类,在做Equals等判断的时候,没有考虑父类的字段。这种情况还需要大家自己再打一下@EqualsAndHashCode注解,并且把callSuper打开。

Kotlin与Lombok的集成问题

Kotlin与Lombok不同,它有单独的Compiler,执行的时机早于Lombok的注解工具执行,这会导致如果想在Kotlin里调用Lombok的生成的代码,会找不到实现。这个问题可以通过Delombok来解决,Delombok会先将Lombok处理的代码编译到编译后的目录,保障Kotlin能访问到增强后的函数,避免编译失败。 目前Kotlin项目组应该也注意到了这个问题,已经在开发Kotlin的lombok编译插件,其原理是生成stub,避免编译器失败。目前还在实验阶段,注解的支持有限,感兴趣的可以看https://kotlinlang.org/docs/lombok.html

附: 阅读 Lombok项目

Lombok项目是开源的,托管在github上边,地址https://github.com/projectlombok/lombok

Lombok项目是通过ant管理的,并且ant执行的最低版本是Java11。项目下载后在根目录可以使用ant IDE方式为使用的ide生成项目文件,可以使用的是ant eclipse和ant intellij。由于项目组成员全部是eclipse,所以不保证intellij的脚本可以使用,而且,经过测试,它确实不能使用LoL。好在intellij可以使用导入项目的功能,在生成eclipse文档后,当成是eclipse项目导入即可。

由于是Ant项目,跟Maven项目的默认结构差异还是不小的。不过阅读其源代码,主要关注的是src目录下的core目录。我们日常使用的注解就在这个目录下。

目录下还有若干子目录,bytecode是它对字节码操作的一些工具,core是核心处理,javac是它对javac的支持。core\handlers目录下是各个注解的处理方式。

在Mac上打开Iterm或终端时重建tab

长期以来都不知道 Mac 上的 iTerm2 或者自带的 Terminal 程序怎么能在打开的时候恢复上一次关闭的标签,但是如果 Mac 四国重启,发现都是能打开上一次的标签的,说明有这个功能。

今天谷歌了一下,看到很多答案都是在系统设置里,一开始我还以为是答非所问,试了一下才发现所言非虚^_^。

答案就是在系统设置下的通用设置里,不要勾选「退出 App 时关闭窗口」。对于 iTerm,还需要在 General 设置页的 Startup 设置里选择 Use System Windows Restoration Setting。

这样下次打开的窗口的时候就能恢复上次关闭的窗口啦。

Java流水帐代码的重构与单元测试

背景 大多数软件开发者都是学过很多编程思想和知识的人,绝对不是不知道什么代码是好的,什么样的代码是糟糕的. 然而我们在工作的过程中,为了满足和解