protobuf和springboot的结合

protobuf的java example见:protobuf的java学习版

在那篇文章中我们也说了,protobuf仅用于一些对性能要求特别高的场景,缺点就是和传统的java编程还是有一些不耦合的地方,需要进行转换,导致了编程有些麻烦。

在这篇文章中,我们来看一下 protobuf如何和springboot进行结合:

  1. 新建一个maven的springboot工程,在这个工程的pom中加入依赖:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId>
            <version>3.10.0</version>
        </dependency>
        <dependency>
            <groupId>com.googlecode.protobuf-java-format</groupId>
            <artifactId>protobuf-java-format</artifactId>
            <version>1.4</version>
        </dependency>
  1. 本例子还是使用官方的 addressbook.proto 这个例子,对这个文件中的java输出包改成:
    option java_package = “com.champbay.blog.protobuf.model”;
    其他不变,然后在这个目录中运行:protoc –java_out=. addressbook.proto,将生成 com/champbay/blog/protobuf/model/AddressBookProtos.java

  2. 创建一个Configuration来加载 ProtobufHttpMessageConverter 的Bean

package com.champbay.blog.protobuf.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;

@Configuration
public class WebConfig {

    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

}

此 ProtobufHttpMessageConverter 的作用是对 protobuf 进行 encode 和 decode

  1. 创建一个 Rest Controller:
package com.champbay.blog.protobuf.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.champbay.blog.protobuf.model.AddressBookProtos;
import com.champbay.blog.protobuf.model.AddressBookProtos.Person;

@Controller
public class TestController {

    @RequestMapping(value = "/test", consumes = "application/x-protobuf", produces = "application/x-protobuf")
    @ResponseBody
    public AddressBookProtos.Person getPersonProto(@RequestBody AddressBookProtos.Person person) {
        System.out.println("id: " + person.getId());
        System.out.println("name: " + person.getName());
        System.out.println("email: " + person.getEmail());

        //这个地方需要加入将protobuf的对象转换成普通的java pojo的方法 !

        Person.Builder builder = Person.newBuilder();
        builder.setId(2);
        builder.setName("myhello");
        builder.setEmail("myhello@aaa.com");

        return builder.build();
    }

}

从这个类中能看到需要加入将protobuf的对象转换成普通的java pojo的方法,这个方法可以参考: Protobuf与POJO的相互转化 – 通过Json,也能看出protobuf要想方便使用,这个道路还是比较难的,可谓是“鱼和熊掌不可兼得”

5,最后通过 HttpClient 来调用这个 rest。

具体可以见源代码:https://github.com/champbay/protobuf-springboot-test

protobuf的java学习版

protobuf 和 springboot 的相结合,可以参见另外一篇博文 《protobuf和springboot的结合》

protobuf 是 protocal buffers的简称,是google研发的一种数据传输格式。

我们常用的数据格式是json,或者xml,这2种都是文本型格式,特点是容易被人识别,非常容易编程,缺点是数据量有点大。

在有一些场景下,比如帧同步、各个应用之间的同步信息,这样的场景往往是并发高、发送/接收密集,假如用json来传输,那么性能上就会大受影响,所以能否有另外一种数据传输格式,能有效解决这个问题,这就是protobuf上场的原因,protobuf是采用字节流的方式进行传输,通过一些约定使得比json的传输量大为减少。

个人认为,为了使得各种方便,在绝大多数场景下都优先考虑json格式,因为protobuf的格式对于编程、维护都太麻烦 了。这大概就是为什么protobuf出来了这么多年,但是不流行的原因,因为可以用的场景太少太少。

官方自带的protobuf examples,里面融合了很多其他的语言,并且采用的方式用的是 make,不直观,于是我把这个examples中的java部分选出来单独做成一个工程:

  1. 在eclipse中,新建一个java的maven工程,在这个工程的pom中加入依赖:
<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version>3.10.0</version>
</dependency>
  1. https://github.com/protocolbuffers/protobuf/tree/master/examples 中,将 addressbook.proto 下载到该工程中。同时将AddPerson.java和ListPeople.java也下载到工程中。addressbook.proto这个文件是类的定义文件,不是像以前那样直接在java中写java类,而是先写这个定义文件,这个文件的规范见:https://developers.google.com/protocol-buffers/docs/proto3,也可以网上搜索protobuf的中文版规范(都是机器翻译的,勉强能看)。在这个规范中:除了空行和注释以外,syntax = “proto3″必须放在第一行。也就是说第一行可以是空行、注释,紧接着必须是 syntax = “proto3″。
  2. 接着需要有个工具将 addressbook.proto 转换成相应的 java 类,到 https://github.com/protocolbuffers/protobuf/releases 下载对应操作系统的转换工具,下载好后解压即可使用,比如我下载的 protoc-3.10.1-win64.zip 这个,解压好后,需要将路径加入到操作系统的 PATH 中以方便调用,测试一下:
    protoc –version #能显示版本号就表明可以使用
  3. 进入操作系统的console(linux是shell),进入addressbook.proto这个文件所在的文件夹,运行:protoc –java_out=. addressbook.proto,运行后将在当前目录下生成 com/example/tutorial/AddressBookProtos.java这个类,然后查看AddressBookProtos.java这个类,可以看到其中包含2个实体Bean:
com.example.tutorial.AddressBookProtos.AddressBook
com.example.tutorial.AddressBookProtos.Person

从中也可以间接知道,这种写法和传统的java写法有了很大的区别,所以这些就是protobu使用上非常难用的地方。
5. 可以运行 AddPerson.java 和 ListPeople.java 这两个类来看看 protobuf 是如何序列化和反序列化的。

这个工程的源代码见:https://github.com/champbay/protobuf-java-test

在logback中增加正则表达式支持

在一些简单的应用中,logger没有几个,但是在一些比较复杂的应用中,往往有这样的场景需要:

<logger name="com.champbay.aaa.mapper.Aaa" level="DEBUG" additivity="false" />
<logger name="com.champbay.bbb.mapper.Bbb" level="DEBUG" additivity="false" />
<logger name="com.champbay.ccc.mapper.Ccc" level="DEBUG" additivity="false" />
<logger name="com.champbay.ddd.mapper.Ddd" level="DEBUG" additivity="false" />
...
<logger name="com.champbay.zzz.mapper.Zzz" level="DEBUG" additivity="false" />

像上面那么多的logger,能不能用下面的正则来替代呢?

<logger name="com.champbay.*.mapper" level="DEBUG" additivity="false" />

结果是不支持的,那么怎么样在logback中支持这样的需求呢?

首先,看看这篇文章:https://stackoverflow.com/questions/48657071/logback-logger-name-with-wildcard/48664150,它采用了ch.qos.logback.core.filter.EvaluatorFilter和ch.qos.logback.classic.boolex.GEventEvaluator来达成这个目的,很方便,但是这种方法的运行环境需要 groovy 的支持,需要在工程中引入 groovy 包,假如你的工程中本身就用到了 groovy,那自然可以,否则就需要引入 groovy 包,姑且不论操作上是否复杂麻烦,就性能来说也是有点点影响的。

直接参考logback的官方文档:http://logback.qos.ch/manual/filters.html,里面提到了第一种方式方法:继承 ch.qos.logback.core.filter.Filter 这个,我个人觉得这个是最好的解决方案。

看一下实现源文件:

package com.champbay.core.log;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

public class MyLogbackFilter extends Filter<ILoggingEvent> {

    private Pattern pattern;

    private String logPattern;

    public String getLogPattern() {
        return logPattern;
    }

    public void setLogPattern(String logPattern) {
        this.logPattern = logPattern;

        pattern = Pattern.compile(logPattern);
    }

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if(pattern == null)
            return FilterReply.DENY;

        String loggerName = event.getLoggerName();
        if(StringUtils.isBlank(loggerName)) {
            return FilterReply.DENY;
        }

        Matcher matcher = pattern.matcher(loggerName);
        if(matcher.matches()) {
            Level level = event.getLevel();
            if(level.isGreaterOrEqual(Level.DEBUG)) {
                return FilterReply.ACCEPT;
            } else {
                return FilterReply.DENY;
            }
        } else {
            return FilterReply.DENY;
        }
    }

}

    <appender name="myAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="com.champbay.core.log.MyLogbackFilter">
            <logPattern>com.champbay\..*\.mapper\..*</logPattern>
        </filter>
        <file>aaa.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>aaa-%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>xxxxxxxxxxxxxxxxxx</pattern>
        </encoder>
    </appender>

    <logger name="com.champbay" level="DEBUG" additivity="false">
        <appender-ref ref="myAppender" />
    </logger>

应用连接不上zookeeper

有一台测试服务器,上面放了N多的应用,这些应用都要连接zookeeper,有一天发现其中某个应用连接不上zookeeper的2181端口,但是其他的应用却能连接上,排除了程序的问题,开始分析这个问题。

先用telnet xxx.xxx.xxx.xxx 2181看看:
Trying xxx.xxx.xxx.xxx…
Connected to xxx.xxx.xxx.xxx.
Escape character is ‘^]’.
Connection closed by foreign host.
可以看到是连接上了,但是因为什么原因导致了连接被意外中断了。

再看一下连接zookeeper的连接数:
netstat -na|grep xxx.xxx.xxx.xxx:2181|wc -l
10

看到已经连接zookeeper的连接数为10,这个数字很特别啊,为什么刚好是10?难道zookeeper中有设置连接数为10的?通过查找文档,zookeeper还真有这个连接限制:
maxClientCnxns 这个在配置文件中假如不设置的话,默认连接是 10

于是设置 maxClientCnxns=100,并重启后问题解决。

@SpringBootApplication中几点解惑

@SpringBootApplication这个annotation是由3个annotation组合而成:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

假如你愿意,也可以分别用这3个annotation来代替@SpringBootApplication这一个annotation。

在讲下面之前,严重吐槽一下 springboot 的官方文档,它对所涉及的内容都提到那么一点点,但是每一点都是语焉不详,非常的晦涩。

@ComponentScan中的basePackages 和 @SpringBootApplication中的scanBasePackages有什么关系?

我们稍微复杂点的应用程序,一般都有core这样的jar包引入,core.jar包的包名和springboot应用的包名前面部分基本上一样。这个架构如下图所示:

core的包为:
    com.champbay.core

application的结构如下:
    com.champbay.app
    -->com.champbay.app.util
    -->com.champbay.app.service
    -->com.champbay.app.web
    -->com.champbay.app.Application.java

官方文档中有一段描述如下:

We generally recommend that you locate your main application class in a root package above other classes. 
The @SpringBootApplication annotation is often placed on your main class, 
and it implicitly defines a base “search package” for certain items.

上面这段话的描述反映了下面几点:
1,springboot工程有一个main函数的类。
2,这个main函数的类应该位于 com.champbay.app 下面。
3,这个main函数的类上有@SpringBootApplication的注解。
4,这个@SpringBootApplication注解,隐式地表示了凡是在com.champbay.app以及com.champbay.app下面的子包(com.champbay.app.util,com.champbay.app.service,com.champbay.app.web等)中的诸如:@Component,@Repository,@Controller等都将自动扫描。

那么,Application.java启动的时候,会自动扫描com.champbay.app下面的所有子包,但是对于com.champbay.core下面的就不会自动加载。为了解决这个问题,需要在@ComponentScan设置basePackages为com.champbay即可,这样com.champbay.core就会被自动扫描注册那些bean。

我们为了能够自动扫描那些包,需要设置@ComponentScan的属性basePackages,但是总不能因为这样的需求而不用@SpringBootApplication,而用3个annotation来写太麻烦了,所以,在@SpringBootApplication中,定义了一个scanBasePackages,效果和@ComponentScan的basePackages完全一样。我们来看看Spring文档关于这一段的描述,看完后又想XXX了,简直太惜字如金了:

@SpringBootApplication also provides aliases to customize the attributes of @EnableAutoConfiguration and @ComponentScan.
然后呢?然后就没有了

好吧,让我们来看看@SpringBootApplication的源代码吧:

/**
     * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
     * for a type-safe alternative to String-based package names.
     * @return base packages to scan
     * @since 1.3.0
     */
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

如此就完全明白了。scanBasePackages就是basePackages的别名!

我们从上面看到,当在 com.champbay.app 不同包下面,有@Component、@Repository、@Controller的时候,需要指定scanBasePackages。
但是我们从另外一篇文章:Auto-configuration的规范中可以看到,springboot不建议用这种方式来自动加载,而是采用如下的方式:

Spring Boot checks for the presence of a META-INF/spring.factories file within your published jar. 
The file should list your configuration classes under the EnableAutoConfiguration key, as shown in the following example:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

Auto-configurations must be loaded that way only. 
Make sure that they are defined in a specific package space and that they are never the target of component scanning. 
Furthermore, auto-configuration classes should not enable component scanning to find additional components. 
Specific @Imports should be used instead.

也就是说,不要用scanBasePackages这样的方式,而是采用 META-INF/spring.factories 的方式。

activiti usertask中的dueDate不起作用

我们在工作流中,经常有这样的需求:一个用户任务当超过多少时间后,自动进入下一个流程。比如:一个下发通知的流程,需要几个人看通知,有可能个别人员这段时间休假,导致迟迟没有看通知,所以我们希望到多少时间后,该流程能自动到下个任务去统计多少人看了通知,多少人没有看通知。这个时候,我们就需要一个机制让任务能自动结束并转入下一个任务。

我们看到在bpmn中的usertask有一个属性dueDate(到期时间),那我们用这个特性那是不是很方便。我们做了一个最简单的例子后,还需要做好前提条件:需要开启JobExecutor或者AsyncExecutor,这2个的设置请自行查阅相关文章,网上很多。

运行后,我们发现dueDate不起作用,怎么测试都不成功,于是再去查相关资料,这个资料就很稀少了,好像这么有用的一个属性大家居然没有用到?最后在https://stackoverflow.com/questions/35089524/will-activiti-give-call-back-if-due-date-is-completed这里找到说明:

unless you add a boundary timer event in your process that fires at the due date. As far as Activiti is concerned, due date is simply another piece of task and process instance metadata. You can use it, but you need to model alerts.

也就是说:在usertask中的duedate没有用的,假如希望用到到期时间这个功能,那就用 boundary timer event(边界定时事件)。

嗯,这个倒也是如此,用“边界定时事件”确实能满足这个需求。但是,吐槽一下,对于使用者来说,这个是多么会产生误导的啊,所以,强烈建议在 usertask中去掉 dueDate 这个属性。

eclipse离线安装activiti designer

根据activiti官网上介绍说明的,在eclipse安装activiti designer居然失败??!!
报错:https://www.activiti.org/designer/update/content.xml 找不到。

没有办法,只能通过离线方式来安装activiti designer,通过 https://www.activiti.org/designer/archived/activiti-designer-5.18.0.zip 下载,但是想下载其他版本的怎么办?对不起,没有目录列表,只能靠猜了。同时这个地址下载网速很慢,所以依靠我们伟大的迅雷,一会时间就下载完了,迅雷牛X。

下载好后,在eclipse中,help -> install new software -> archive,选中下载的文件,然后name中随便输入一个activiti即可,在 contact all update sites during install to find requied software 前面的checkbox要打上勾,因为可能会对你的eclipse进行升级必要的图形组件。选好后然后点击 finish,根据后面提示安装。

安装的时候,假如报错的话,那么可能你的eclipse版本过低了,最好下载一个最新的eclipse。我用的是一个比较新的eclipse,所以在安装的时候,没有其他额外的组件需要安装(都有了),所以安装的速度非常快,没有任何问题。

网上说的其他各种各样的安装法,看了都头痛,但是我这种办法是最有效最便捷最高效的。

e.printStackTrace()没有打印出完整的堆栈

单位里的一个很老的应用,在分析一个问题的时候,查看日志,看到打印异常的地方仅仅出现了一句:java.lang.NullPointerException。

该应用的源代码都已经找不到了(真的太老了),只能反编译class文件看,发现就是一句 e.printStackTrace(), 那也应该打印出堆栈啊,为什么堆栈没有打出来?

找到网上的几篇文章:
java.lang.NullPointerException:null 没有打印出任何堆栈信息的解决办法
NullPointerException in Java with no StackTrace

需要在java的启动命令中加入:
-XX:-OmitStackTraceInFastThrow

假如是 tomcat 的应用,那么需要在 bin/catalina.sh 中,大致如下位置,加入:

# $Id: catalina.sh 1146097 2011-07-13 15:25:05Z markt $
# -----------------------------------------------------------------------------

#在这个位置加入下面这一行
JAVA_OPTS=" -XX:-OmitStackTraceInFastThrow"

# OS specific support.  $var _must_ be set to either true or false.
cygwin=false
os400=false

重启后,堆栈出现了!

对这篇文章进行3个补充:
1,堆栈被丢弃,不单单出现在 e.printStackTrace() 中,还出现在 log4j,logback 中,比如打印日志如下:
logger.error(e.getMessage(), e);
这样,往往e的堆栈在上述情况下会不打印完整。
2,到目前为止,我只看到过 java.lang.NullPointerException:null 这样的异常,其他的异常发生堆栈被丢弃没有遇到过。
3,堆栈被丢弃,不仅仅发生在老版本的jdk上,新版本的jdk都会出现,也就是说jdk5,jdk6,jdk7,jdk8都会出现。

集群下的quartz出现重复调度的问题解决

我们采用 xxl-job 来作为我们的调度任务中心,而 xxl-job 采用的是 quartz 作为它的调度内核。

我们在部署的时候,采用了2台服务器集群的方式,有一天在分析一个工单的时候发现这2台服务器同时执行了某个时间段的调度2次,于是开始了这个问题查找分析过程。

首先我们找到了这篇文章:多个xxl-job-admin节点,任务重复执行问题,这篇文章描述了为什么会发生这个问题,以及该如何解决,但是这篇文章写得比较简短,还是不明白。

接着我们找到了这篇文章:记一次Quartz重复调度(任务重复执行)的问题排查,这篇文章的作者花了很多心思写这篇文章,写得很透彻。我们通过一边参照他的文章,一边看源代码,一边再进行不同阶段跟踪测试,证实了这篇文章所写得完全正确,也证实了他的解决办法是正确的。

下面就这个问题的出现原因再啰嗦几句,希望能更进一步的解惑:
下面这张图非常重要,我这边摘抄过来(链接地址没法用,所以我摘抄过来,希望作者理解一下这里用来说明问题):

  1. 上面图中出现的那几个Lock,都是基于数据库的分布式锁,这个分布式锁采用了数据库的for update行级锁的机制来实现。这个机制保证了在一开始的时候只有1个调度服务才能获取到调度任务。

  2. 上图中的第一个Lock,非常关键,这个Lock是出现重复调度的关键所在,但是 quartz 默认这个 Lock 是空的!!!这就会出现在上面文中所说的问题,2个调度服务同时发出请求,第1个非常快速的完成,会导致第2个也能在瞬间拿到。具体可以参见这个过程:

  3. 上图中的作者,有部分语言描述有丁点漏洞,上图中的“线程A”,“线程B”,会让读者认为只是在一个进程中会出现重复调度的问题,其实不然,应该改成“进程A”,”进程B”

springboot下报错“MethodInvokingJobDetailFactoryBean is not serializable”

quartz是在java语言中用得最为广泛的任务调度框架,spring对quartz进行了集成,方便了开发和使用。

在springboot下,我们对一个采用quartz的app进行多个集群部署的时候,对于quartz的集群采用数据库集群方式,报了“MethodInvokingJobDetailFactoryBean is not serializable”的错误。

原因是spring为了让开发更方便,耦合性更低,对quartz中 JobDetailFactoryBean 用 MethodInvokingJobDetailFactoryBean 进行了封装,假如quartz是单机运行那是没有关系的,但是当采用数据库方式进行集群的时候,quartz会把 JobDetailFactoryBean 序列化存储到数据库中,假如用 MethodInvokingJobDetailFactoryBean 的话,就会报错,说不能序列化。

解决的办法参考这篇文章:使用Spring + quartz集群持久化时注意事项,但是这篇文章中只讲了在spring下如何修改。

下面讲一下在 springboot 中如何修改:
一、需要将原先定义的pojo 的 Job 改成实现 Job 接口:

@Component("myJobHandler")
public class MyJobHandler implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("do it ......");
    }

}

二、在配置中如下设置:

@Bean(name = "myJobDetailFactoryBean")
    public JobDetailFactoryBean getMyJobDetailFactoryBean() {
        JobDetailFactoryBean jobDetail = new JobDetailFactoryBean();
        jobDetail.setJobClass(com.champbay.quartztest.MyJobHandler.class);
        jobDetail.setDurability(true);
        return jobDetail;
    }

    @Bean(name = "myCronTriggerFactoryBean")
    public CronTriggerFactoryBean getMyCronTriggerFactoryBean(
            @Qualifier("myJobDetailFactoryBean") JobDetailFactoryBean jobDetail) {
        CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
        tigger.setJobDetail(jobDetail.getObject());
        // 每1分钟     
        tigger.setCronExpression("0 0/1 * * * ?");
        return tigger;
    }

本文结束。