程序中大都需要定义各种配置,诸如数据库连接之类的,最近也需要开发Web框架,于是也想找个比较好用的配置文件格式。搞来搞去,发现都不是很喜欢。先来看一下几种常见的配置文件格式吧:

Properties

Java所带来的Properties文件可能是用的比较多的格式了吧,就是一个简单的key-value的文本文件,但是缺点也很明显:

  • Unicode需要转码,看着不是很蛋疼么?
  • 不支持数组类型,所以以前经常会用key.1,key.2...key.n这样的方式来遍历得到一个数组
  • 扁平结构,如果碰到一些比较长的key就有点不好看了(比如SpringCloud的配置,spring.jpa.datasource.xxx)写起来比较麻烦
spring.data.mongodb.host= localhost
spring.data.mongodb.port=27017 # the connection port (defaults to 27107)
spring.data.mongodb.uri=mongodb://localhost/test # connection URL
spring.data.mongo.repositories.enabled=true # if spring data repository support is enabled

Yaml/TOML

Yaml好像很流行的样子,我们在springcloud的项目中大量使用,但是说实话这个格式我也不喜欢,为啥?

  • 依赖于缩进,复制粘贴的时候麻烦了
  • 语法有点复杂了
    TOML感觉和YAML差不多,也挺复杂的样子。
# Zuul
zuul:
  host:
    connect-timeout-millis: 50000
    socket-timeout-millis: 10000


# Hystrix
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000

Ini

Windows所带来的格式,优点是可以带分组,好像比Properties文件更舒服一点,但是对于上面提到的缺点也有。

[curentUser]      ;  this is a Section
name=wisdo     ; this is Parameters
organization=cnblogs   ; this is Parameters
 
[database] 
server=127.0.0.0   ; use IP address in case network name resolution is not working 
port=143 
file = "user.dat" 

JSON/LUA

Json的缺点在于你要用很多个引号,同时最大的问题在于不支持注释。Lua可能是我最想用的脚本了,但是在Java中使用也比较麻烦,尤其是我想手写一个配置文件解析器,这样就麻烦了(主要是不会)。

还有Ini + Json的方法,但是感觉也比较丑,于是想来想去,还不如按照自己的意愿发明一种配置文件格式好了,主要有以下的考虑:

  • 语法应该简单,不需要依赖缩进
  • 支持数组
  • 支持使用变量(类似shell)
  • 支持Unicode,中文直接写,所见即所得
  • 支持某种形式的命名空间(类似ini中的section)来对配置进行分组
  • 支持注释
  • 支持多行字符串
  • 格式好看...

目前正在计划中,准备利用Antlr实现解析。

在一个“敏捷”的团队,写注释被认作是一个不好的习惯,因为他们认为,

Good programming is self-explanatory. Bad Programming requires explanation
总结一下,认为程序中不需要写注释的原因主要有如下的几点:

  • 需要写注释的程序说明代码不够清晰啊,可以可以通过重构的方式,让代码变得“可读”
  • 维护注释是一件工作量很大的事情,改完代码之后,时常会忘记修改注释
  • 注释如果解释的不清楚,那就需要“注释的注释”...
  • ……
    不能不说这些没有道理,实际上也都是很关心的问题,代码写的更好更可读,当然是值得推崇的。并且诚如所言,代码应该是“自解释”的,大部分情况下,我们可能的确不需要注释。代码的可读性,和注释,目的都是一样的,让别人看得懂,不会掉坑里面。这里的坑,可能是代码逻辑的,可能是业务逻辑的,可能是某个库的bug,可能是某种奇怪的设计或者历史原因。

所以说,有另外一个更重要的他们没有考虑到的就是:

self-explanatory code only tell how it is working. It rarely tells how it should work.

正好最近又遇到一次坑。来描述一下这个故事:
起因是我们系统需要从一个第三方系统中查询数据。这个系统调用,我们代码里面是这么写的:

try {
    return client.getVehicleBaseData(finOrVin);
} catch (Exception e) {
    log.error("error loading vehicle basic data from eva for finOrVin:{}", finOrVin);
    throw new EvaAccessFailureException(evaLoadService.generateFallback(e.getMessage()));
}

这段代码的功能是,调用外部系统的api,然后返回一个结果;如果出错则抛出异常。同时,需要根据出错的“代码”来判断是对方系统的内部错误,还是资源找不到。

Fallback generateFallback(String message) {
    try {
        int startPos = message.indexOf("{\"error\":");
        if (startPos == -1) {
            return new Fallback(UNEXPECTED, message);
        }
        EvaErrorResponse response = JsonUtils.unmarshal(message.substring(startPos), EvaErrorResponse.class);
        return new Fallback(getByStringValue(response.getError().getErrorCode()));
    } catch (Exception e) {
        log.error("unexpected error message from EVA {}", message);
        log.error(e.getMessage(), e);
        return new Fallback(UNEXPECTED, message);
    }
}

这段代码尝试从message里面解析一串error,然后再反序列化为JSON,这里是这个EvaErrorResponse的定义:

@Data
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EvaErrorResponse {
    private Error error;
}

@Data
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class Error {
    private String errorCode;
    private String errorDesc;
}

姑且不说一个简单的Bean用这么多Lombok注解的问题~ 然后我需要做的是,模拟这个系统的出错返回,因为我们的开发环境无法连真实的三方系统测试。那么问题来了,三方系统出错的时候,应该返回什么呢?

首先问问写这个代码的人(也就是直接对接这个系统的人)吧。他给了我一个文档,文档里面是这么描述的:
error_3rd.png

那么问题来了,这和代码定义完全不一样啊!然后告诉我以代码为准。从这个代码根本无法确定错误返回结构。然后又看看我们这个模拟的stub的代码,关于出错的地方是这么定义的:

public class OabResponseDto {

    private boolean success;

    private Object result;

    private String error;

后来才觉察到,这是另一个系统的接口返回了。但几个系统的模拟stub都写到了一起,让人完全无法确定真实的三方接口定义。最终,我找到了调用这个接口的测试环境,自己调用了一次,原来结果是这样的:

{"error":{"errorCode":"WDCS0003","errorDesc":"Resource not available!"}}

这耗费了我半天的时间。于是为了避免有人再踩这种坑,我加了个注释在这里:

Fallback generateFallback(String message) {
    try {
        /**
         * example actual response from eva:
         * {"error":{"errorCode":"WDCS0003","errorDesc":"Resource not available!"}}
         */
        int startPos = message.indexOf("{\"error\":");

然而这又被批判了,理由是这段注释不能解释代码。因为这里message并不是这样。那message到底是什么样?他们说,你可以调试打个断点看。难道让每一个看代码的人都打个断点来看么,这是什么逻辑!我就呵呵了。最终我还是妥协了,删了呗,对我而言无任何影响。

很多人,如同我的同事,似乎觉得lombok这玩意就像神一样的存在,“极大”的方便了项目的开发。我个人是不喜欢这玩意的,很简单的理由:

  • 生成getter/setter不是多么困难的事情,IDE很简单就能帮你搞定
  • 我不喜欢为自己的IDE装一大堆插件,还要为项目手动开启一下Annotation Processing
  • 代码不可见,意味着生成的getters/setter方法,以及@AllArgConstructor生成的方法无法维护

当我把这些想法告诉同事的时候,同事们都觉得我脑子有问题,理由不充分,”lombok只是一个工具,只是没有找到使用工具的最佳实践“。实际上对于技术人员来说,想说服别人是很困难的事情,然而我们为什么要试图说服别人呢?没有多大的意义。相比于工具,有一些更重要的东西就是:经验和原则。

就我的经验,能简单的事情就不要复杂化,越是复杂越难以维护。当然也有人和我是相同的观点,看了一些有意思的关于为什么不用lombok的讨论,贴出来看看:

OK, let me put it one more time: this has caused me too many bugs. Let me tell
you my past experiences with Lombok, as this is the root of the issue.

On one project, a new version of the Lombok plugin caused the IDE to
crash (I think this was Intellij). So nobody could work anymore. On
another project, Lombok made the CI server crash (and would have
probably caused the production server to crash), as it triggered a bug
in the JVM On a third project, we achieved 30% performance increase by
recoding the equals/hashcode from Lombok
-> In those 3 projects, some developer gained 5 minutes, and I spent hours recoding everything. So yes, a bad experience.

Then, for JHipster, the story is also that we can't ask people to
install a plugin on their IDE:

1st goal is to have a smooth experience: you generate the app and it
works in your IDE, by default 2nd goal is that you can use whatever
IDE you want. And some people have very exotic things, for example I
just tried https://codenvy.com/ -> no plugin for this one, of course

Oh, and I just got 2 more:

Lombok crashing with MapStruct Lombok making Jacoco fails, which meant
the project didn't pass the Sonar quality gate

参考阅读:

? wildcard(通配符)

使用 ? 修饰符可以用作类型转换,List<?> 意味着是一个未知类型的List,可能是List<A> 也可能是List<B>

private final List<String> strList = Arrays.asList("Hello", "World!");
private final List<Integer> intList = Arrays.asList(1, 2, 3);
private final List<Float> floatList = Arrays.asList(1.1f, 2.1f, 3.1f);
private final List<Number> numberList = Arrays.asList(1, 1.0f, 3000L);

public void cast() {
    List<?> unknownList = null;
    unknownList = strList;
    unknownList = intList;
    unknownList = floatList;
    unknownList = numberList;

    for (int i = 0; i < unknownList.size(); i++) {
        // Number item = unknownList.get(i); wrong! 
        Object item = unknownList.get(i);
        System.out.println(item + "(" + item.getClass() + ")");
    }
}
/* output
1(class java.lang.Integer)
1.0(class java.lang.Float)
3000(class java.lang.Long)
*/

这里因为无法确定unknownList的类型,因此从中取出的元素只能用Object去标识,即使可以确切的知道每个元素的类型。那么,我们现在想设置List里面的值,怎么办?

for (int i = 0; i < unknownList.size(); i++) {
    //Number item = unknownList.get(i); Error: incompatible types: capture#1 of ? cannot be converted to java.lang.Number
    Object item = unknownList.get(i);
    // unknownList.set(i, item); Error: java.lang.Object cannot be converted to capture#1 of ?
    System.out.println(item + "(" + item.getClass() + ")");
}

这样连Set 传入一个Object都不行!天呐。因此需要这样做:

changeValueHelper(unknownList);
}

private static <T> void changeValueHelper(List<T> list){
    for(int i = 0 ; i < list.size(); i++){
        list.set(i, list.get(i));
    }
}

extends VS super

实际上泛型仅仅是为了做一个编译时的检查,从逻辑上确保程序是类型安全的。假设我们有这样的类定义:
Object->Parent->T->Child
我们有这样几种写法:

  • List<?> 代表一种未知类型的List,可能是List<Object>,也可能是List<Child>,都可以
  • List<? extends T> 代表T或者T的子类的List,可以是List<T>,也可以是List<Child>
  • List<? super T> 代表T或者T的父类的List,可以是List<T>,List<Parent>,List<Object>

我们有一个事实就是,Child是一定可以转化T或者Parent的,但是一个T不一定能转化成Child,因为可能会是别的子类。
比如我们现在做两个列表的拷贝,

public static <T> void copy(List dest, List src)

想实现从一个列表拷贝到另一个列表,比如

List<Parent> parents;
List<T> ts;
List<Child> childs;

基于上面说的类的继承的事实,ts/childs显然是可以转化成parents的,但是ts无法确保能转化成childs。因此我们的拷贝方法要这样定义:

public class Collections { 
  public static <T> void copy  
  ( List<? super T> dest, List<? extends T> src) {  // uses bounded wildcards 
      for (int i=0; i<src.size(); i++) 
        dest.set(i,src.get(i)); 
  } 
}

因为在desc.set()方法中,需要的是一个能够转化为T的对象的,src中<? extends T> 保证了src中的元素一定是一个T。

多个限制

买了一堆书,学习Ing...

Java编程思想(英文版•第4版)
Effective Java(第2版)(英文版)
Scala函数式编程

Java多线程编程核心技术
Java并发编程实战
Java虚拟机规范(Java SE 8版)(英文版)
深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)
HotSpot实战

大型网站技术架构:核心原理与案例分析
深入分析Java Web技术内幕(修订版)
区块链核心算法解析

STL源码剖析
深入理解计算机系统(原书第3版)
深入理解LINUX内核(第3版)(涵盖2.6版)
Linux内核设计与实现(原书第3版)

Android编程权威指南(第3版)

麻省理工学院(MIT)机器视觉课程指定教材:机器视觉
OpenGL编程指南(原书第9版)

曾国藩传
一句顶一万句(典藏版)

待购买

  • 褚时健传
  • 把信送给加西亚
  • 东野圭吾作品:嫌疑人X的献身
  • The Complete Sherlock Holmes: All 4 Novels and 56 Short Stories(套装,全两册)
  • 瓦尔登湖(珍藏版)(买中文版赠英文版)
  • 简爱(中英对照全译本)
  • 傲慢与偏见(中英对照)(全译本)