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>

Centos7下升级GCC

centos7自带的GCC是比较老的,gcc -v可以看到版本是4.8.5,在yum中也能看到最高版本也是这个版本4.8.5:yum list gcc。

用这个比较老的GCC版本,在运行一些软件的时候,常会报错:
CXXABI_1.3.8 not found
GLIBCXX_3.4.20 not found

用下面2个命令可以看到上述这2个确实不存在
strings /usr/lib64/libstdc++.so.6 | grep GLIBC
strings /usr/lib64/libstdc++.so.6 | grep CXXABI

解决这个问题,只有一条路可以走,那就是升级GCC到更高的版本,GCC官网上的版本已经到了很高的版本9了,我个人觉得没有必要用这么高的版本,折衷一下,选个GCC 6.5.0 这个版本即可。

下面是GCC的升级之路,整个过程大概耗时3个小时,要有心理准备:
1,下载GCC 6.5.0:http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-6.5.0/gcc-6.5.0.tar.gz,或者到官网找更好的mirror下载,https://gcc.gnu.org/mirrors.html,我选了个日本的镜像,很遗憾没有国内的。
2,假设 gcc-6.5.0.tar.gz 在 /opt 下面,tar xzvf gcc-6.5.0.tar.gz && cd gcc-6.5.0
3,升级之前,需要做好下面2个准备工作:
3.a:yum -y install gcc-c++ , 否则在升级的时候会报错:error: C++ preprocessor “/lib/cpp” fails sanity check
3.b:cd /opt/gcc-6.5.0,然后运行 sh contrib/download_prerequisites,必须在/opt/gcc-6.5.0下面,否则运行download_prerequisites的时候就会报错;这个脚本将会下载4个小文件(每个文件1M左右),但是因为网络的因素,下载了半天才下载完。这个必须要运行,否则在GCC升级的时候就会报错什么:GMP 4.2+, MPFR 2.4.0+ and MPC 0.8.0+这些找不到。
4,准备工作做好了,开始吧:./configure –disable-multilib,disable-multilib表示仅仅需要64位的GCC,不需要32位的GCC,假如在centos7安装的时候没有选上32位的GCC,那么在这里必须要 –disable-multilib,否则就会报错。
5,make,这个过程最长了,估计要1个多小时。
6,make install,非常快
7,装好后,gcc -v,确实已经升级到了 6.5.0 版本,但是假如运行前面报错的软件的话,还是会报以前的错误,用刚才的strings命令去看,也是缺少那2个,这是什么原因呢?
8,这是因为,gcc升级安装在了 /usr/local/lib64下面,而运行的软件只认 /usr/lib64 这个位置,所以最后还需要 cp /usr/local/lib64/libstdc++.so.6 /usr/lib64/,这样就全部搞定了

最后说一下,GCC的升级就是比较耗时点,但是难度不大,严格按照我上面的步骤,很容易搞定的。

ssh登录的时候很慢

在ssh登录的时候很慢,网上查找问题的时候,基本上都是前篇一律的,按照网上的来解决:

vi /etc/ssh/sshd_config      (编辑配置文件) 
输入 / ,查找GSSAPIAuthentication 赋值为no 
输入 /,查找UseDNS,赋值为 no(该项默认不启用的,要把前面的#删除掉) 
最后输入!wq保存文件并退出 
重启systemctl restart sshd

但是用上面的方法修改后,发现有些起效,有些还是不起效。继续查找这个问题,参照这篇文章 linux下ssh连接慢的原因调查及解决方案

systemctl restart systemd-logind

问题解决。

总结一下:除了需要设置 UseDNS 和 GSSAPIAuthentication 以外,最好 systemctl restart systemd-logind,这样就可以彻底解决这个问题了。

应用连接不上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 的方式。

golang的继承实现

go的继承叫做“匿名组合”,这个不等同于组合,也就是说:“匿名组合”是“匿名组合”,“组合”是“组合”,两个完全不一样。下面看一个最简单的组合,然后再比较后面讲到的“匿名组合”,会发现两者不同。

type A struct {
}
func (a *A) Test() {
}

type B struct {
    a A
}
func (b *B) Test() {
    a.Test()
}

var b = new(B)
b.Test()

从上面可以看到“组合”的特点,要调用A的Test()方法,则需要用B的某个方法给封装一下,有没有一种更便利的方法,不用在B中写一个包装方法来调用,能不能省略掉B中的Test()包装方法呢?且看后面。

go的“匿名组合”,有三种实现形式:

1,匿名组合struct

type Human struct {
    Name string
}
func (a *Human) Test1() {}
type Student struct {
    Human
    Age int
}
func (a *Student) Test2() {}

var a = new(Student)
a.Name = "zhangsan"
a.Age = 11
a.Test2()
a.Test1() == a.Human.Test1()  //这2种方式都可以

2,匿名组合指向struct的指针

type Hobby struct {
    Name string
}
func (a *Hobby) Test1() {}

type Student struct {
    *Hobby
    Age int
}

var h = &Hobby{"movie"}
var s = &Student{h,18}
s.Test1()

初看这种方式和第一种匿名组合struct没有什么区别,而且更麻烦了,在创建Student的时候,还要对其中的*Hobby进行赋值。但是细细一想,这种模式的优点很大,这种类型的“匿名组合”实际上是第一种的“匿名组合”stuct再加上”组合“这2种模式而成。

对于第一种”匿名组合“struct,往往用在派生类和父类有一定继承关系上,而对于第二种的”匿名组合“指向struct的指针来说,派生类中的组合对象可以和父类没有太大的关系或者没有任何关系,就是一个纯粹的”组合“模式,上面讲到了,在组合模式中要调用父类的方法,需要在派生类中再写一个包装方法,很是麻烦。刚好,用第二种的”匿名组合“指向struct的指针这种模式来说,非常好的解决了这个问题,非常完美。

上面的例子假如看得还不是特别明白的话,好好体会下面这个例子就非常清楚了,这个模式非常不错:

package main

import (
    "fmt"
    "log"
    "os"
)

type MyJob struct {
    Command string
    *log.Logger
}

func (job *MyJob) Start() {
    job.Println("job started!") // job.Logger.Println

    fmt.Println(job.Command)

    job.Println("job finished!") // job.Logger.Println
}

func main() {
    logFile, err := os.OpenFile("./job.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0)
    if err != nil {
        fmt.Println("%s", err.Error())
        return
    }
    defer logFile.Close()

    logger := log.New(logFile, "[info]", log.Ldate|log.Ltime|log.Llongfile)
    job := MyJob{"programming", logger}

    job.Start()
    job.Println("test finished!") // job.Logger.Println
}

因为 MyJob 内组合的*log.Logger和MyJob关系不是纯粹的继承,假如将 log.Logger 采用组合的方式,那么还需要对 MyJob 再定义一个 Println 或者一系列方法,而采用”匿名组合“指向struct指针的这种方法就有效的解决了这些问题,细细体会。

3,匿名组合接口

这个和第2种模式几乎一样,也需要在实例化派生类的时候,要对其中的匿名组合进行接口实现类的赋值,道理和第2种一样。

云服务器的centos7镜像安装gnome以及vnc

这篇文章也适用于其他的云服务器。

有个别场景需要用图形的方式连接linode,linode最便宜的vps就是1GRAM,所以完全可以用来支持图形界面了,网上有不少关于linode vps安装图形界面的教程,但是那些都有些老了,最新的linode vps在centos7下安装图形界面非常简单,过程大致如下:

1,首先看一下centos7是否有 GNOME Desktop 这个安装组:yum grouplist
2,yum groupinstall GNOME Desktop
3,yum install vnc vnc-server
4,vncpasswd //设置vncserver的登录密码
5,vncserver -depth 24 //无需vncserver那些很复杂的加入启动服务中的那些过程
如上,就启动默认 :1 号vncserver,监听端口是 5901,有可能5901已经被其他进程占用了,那么vncserver就会启用 :2号,监听端口是5902,同样的道理假如5902也被占用,那么继续顺延,我就遇到了这样的极端情况 5901到5903都被占用了,结果就启用了:4号,监听端口是5904。
vncserver -kill :1 //这个命令是杀掉vncserver进程
6,客户端下载 realvnc,然后连接 ip:1 就可以连接上图形界面了

是不是很简单:)

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 这个属性。