根据条件过滤日志输出

1 条评论

一个在很多项目里可能存在的场景:各种原因导致了很多人在共用一套环境,然后要在这一套环境上进行各种开发、调测、定位活动,由于用的人多,查看日志就变得很麻烦,首先要从极速刷新的日志里先找到关注的关键字,然后从关键字的位置再找出处理这一请求的上下文日志来,经常重复这样的操作,严重地影响了工作效率。

所以最近想花时间来解决这个问题,思路是用户在浏览器或者命令行上发起查看日志的请求,同时指定自己查看日志的过滤条件,这个过滤条件针对的是日志框架的 MDC(Mapped Diagnostic Context)信息,这些MDC信息来自于请求参数,客户端特征信息,也可以是业务逻辑自行指定,然后再拦截日志框架的日志输出,所有的日志输出都和用户的过滤条件进行匹配,匹配上了就流式地返回给用户。这样用户就可以单独地收到一份只包含符合特定过滤条件的日志了。比如,用户可以指定只查看某个客户端IP请求的日志内容,大大提升日志查看的效率。

当前的实现方案,选择了基于 tomcat 对外暴露一个 websocket 的接口(为了便于扩展,以及满足简单、优雅地返回流式数据的要求)用来接受用户的请求,然后在 logback 的所有 Logger 上额外注册一个 Appener,拦截到所有日志后,根据用户请求的过滤条件过滤日志信息,并不断返回响应。同时,也自动注册了一个 Web Filter,这个 Filter 会将 http 请求的很多信息注册到 MDC里并在请求结束时自动清理,也试验性地增加了一个非常简单的 web 页面,可以访问这个http页面,带上过滤参数,然后过滤后的日志就不断地刷新出来了。项目的使用非常简单,一个 jar 包扔到 web 应用里,不用做其他任何事情了。

Cat

由于是 websocket 的接口,所以应该可以非常方便地做出一个方便好用的web页面来,而对于开发人员来说,可能有大部分在后台查看日志的场景,后续会补上一个用go语言封装的简单命令,支持在后台直接查看日志,类似这样: catlog -s "ws://127.0.0.1/context/logcat" -c a=1

项目地址在:  https://github.com/qiyi/cat 

SpringBoot应用集成etcd配置源

0 条评论

在分布式、云化的系统里,应用的配置(尤其是依赖服务的配置、环境相关的配置)都存储到应用的本地配置文件里会给维护带来很大的麻烦,而且 docker 更是将应用本身做成了镜像,更难以在本地的配置文件里去存储一些部署环境相关的信息。所以通常在整个系统里会有一个公共的配置服务,配置服务统一集中地维护其他系统的配置信息,再通过网络分发。Spring Cloud Config 就是 Spring 推出的解决方案,不过在自己的应用里还不想为此再起 Java 进程,就选择了较为轻量级的 etcd 来作为配置服务。集成过程如下:

1. 添加 Spring Cloud Etcd 依赖(还没有正式发布版本)

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-etcd-config</artifactId>
  <version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

2. 指定当前的应用名称

在 application.properties 里增加

spring.application.name=pink

3. 在 etcd 上配置数据

在 etcd 的 /config/application 或者 /config/{application.name} 两个节点下,添加类似于 key=value 的子节点数据即可。

Spring Cloud Etcd 注册了 Bootstrap Configuration,这些 Java Config 类会被 Spring Boot 应用在 bootstrap 阶段加载,而 Spring Cloud Etcd 就在此时向 bootstrap ApplicationContext 里注册了 EtcdPropertySourceLocator,在 Spring Boot 应用的 ApplicationContext refresh 之前, Spring Boot 应用本身会注册一个 PropertySourceBootstrapConfiguration,这是一个 ApplicationContextInitializer,他负责找出所有的 PropertySourceLocator,并允许这些 PropertySourceLocator 去初始化 Environment,EtcdPropertySourceLocator 就在此时构造了基于 etcd 实现的 EtcdPropertySource(默认请求 http://localhost:4001),并添加到了 Environment 里,此后 Spring Boot 应用初始化过程中就完全不再感知配置的加载,从 Environment 里获取即可。

SpringBoot应用配置项加密

2 条评论

在实际的应用当中,我们可能会存在一些需要加密存储的配置项,最典型的就是数据库的密码。推荐的一种方案是 jasypt-spring-boot 项目,不过 Spring Cloud 本身也提供了对加密配置项的支持。

首先引入 spring-cloud-context 的依赖 org.springframework.cloud:spring-cloud-context:1.2.0.RELEASE, 然后在 resources 目录下新建 bootstrap.properties, 里面写上加密数据库的密钥内容, 由于默认情况下启用的是 AES128 的算法,因此密钥长度为 16,

encrypt.key=nxN8cE/CoUxxexPh

接下来,就可以在 application.properties 文件里直接写上加密后的密文了,默认情况下加解密算法为 AES/CBC/PKCS5Padding, 使用的 salt 为 deadbeef(16进制字符串格式), 由于 AES 加密结果为字节数组,并不一定是可见字符,所以结果需要再次转换成16进制的字符串。配置格式为 key={cipher}加密后的hex字符串,由 {cipher} 开头,表明后面是加密的配置值,样例如:

spring.datasource.password={cipher}fb748dce88b94fb7d84a9f32e6b5d51729049792d1ee38e2240b176ec5db91cb

Spring Boot 应用启动时,spring-cloud-context 模块会注册一个  BootstrapConfiguration,BootstrapConfiguration 会在 SpringBoot 应用的很早期进行加载,加载时会检测 Environment 里是否存在 encrypt.key 的配置项,如果存在就将其作为加解密的密钥,然后再次注册一个 ApplicationInitializer,由这个 ApplicationInitializer 在 SpringContext 初始化过程当中,对所有配置项值为 {cipher} 开头的配置项值进行解密,解密的结果添加到一个新的 decrypted 的 PropertySource,插入到 Environment 的最前面,于是后面的应用、Bean 初始化过程当中,直接可以根据配置项名称获取到解密的结果了。

用起来特别简单,spring-cloud-context 也基于 Spring 已有的机制,几乎无缝、透明地提供了这个能力,不过直接把密钥的明文写到配置文件里也不太安全,推荐的做法是将密钥分开存储到配置文件和代码里,应用启动时再把两部分呢拼接起来,以便提高安全性,可以参考 PinkApp 的做法

retrofit 服务自定义签名认证

0 条评论

因为用在一个不是特别主流的方案里,所以还是记录下。服务所有的请求通过 form data 格式传输,然后每一次的请求都需要使用服务端给客户端分配的密钥对请求参数进行签名,

直接基于 OkHttpClient 扩展 Interceptor 实现,如下:

  1. public class SignInterceptor implements Interceptor {
  2.     private final Mac mac;
  3.     private final String clientId;
  4.     public SignInterceptor(String clientId, String clientKey) throws NoSuchAlgorithmException, InvalidKeyException {
  5.         this.clientId = clientId;
  6.         byte[] key = clientKey.getBytes(StandardCharsets.UTF_8);
  7.         SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
  8.         this.mac = Mac.getInstance("HmacSHA256");
  9.         this.mac.init(keySpec);
  10.     }
  11.     @Override
  12.     public Response intercept(Chain chain) throws IOException {
  13.         Request request = chain.request();
  14.         RequestBody requestBody = request.body();
  15.         if (requestBody instanceof FormBody) {
  16.             FormBody originFormBody = (FormBody) requestBody;
  17.             FormBody.Builder formBody = new FormBody.Builder();
  18.             List fields = new ArrayList<>(originFormBody.size());
  19.             int size = originFormBody.size();
  20.             for (int i = 0; i < size; i++) {
  21.                 String encodedName = originFormBody.encodedName(i);
  22.                 String encodedValue = originFormBody.encodedValue(i);
  23.                 fields.add(encodedName + "=" + encodedValue);
  24.                 formBody.addEncoded(encodedName, encodedValue);
  25.             }
  26.             Collections.sort(fields);
  27.             String baseString = String.join("&", fields);
  28.             Base64.Encoder encoder = Base64.getEncoder();
  29.             String signature = encoder.encodeToString(
  30.                     mac.doFinal(baseString.getBytes(StandardCharsets.UTF_8)));
  31.             formBody.add("clientId", this.clientId);
  32.             formBody.add("signature", signature);
  33.             request = request.newBuilder()
  34.                     .method(request.method(),
  35.                             formBody.build())
  36.                     .build();
  37.         }
  38.         return chain.proceed(request);
  39.     }
  40. }

然后在构建服务的时候添加进去:

  1. Retrofit retrofit = new Retrofit.Builder()
  2.         .baseUrl("http://127.0.0.1:8080/")
  3.         .addConverterFactory(new FormBodyConverterFactory())
  4.         .addConverterFactory(GsonConverterFactory.create())
  5.         .client(new OkHttpClient.Builder()
  6.                 .addInterceptor(new SignInterceptor("dev", "key"))
  7.                 .build())
  8.         .build();

retrofit 发起form data请求时使用自定义对象参数

0 条评论

Retrofit 在发起 form data 请求时,接口的定义在只能把所有的参数都挨个列出来,用 @Field 进行修饰,但是在参数较多时,显然不现实,过多参数不符合编程规范的。还有一种方式是使用 @FieldMap 注解,此时参数必须是 Map,这样一来就丧失了静态检查的好处,毕竟 retrofit 的一大优势就是使用定义好的接口来代理 http 请求。

理想中当然是可以把 form data 的各个参数定义成对象的字段,于是基于 retrofit 原本的机制扩展了一个 @FormBody 注解,然后用 Converter 来处理 @FormBody 修饰的参数,使用方法如下,:

首先定义接口服务时,给参数加上 @FormBody 以及 @Body 注解

public interface AccountService {

    @POST("/accounts")
    Call addAccountInfo(@Body @FormBody AccountInfo accountInfo);
}

然后创建 Retrofit 时,添加对应的 Convertor

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://127.0.0.1:8080/")
        .addConverterFactory(new FormBodyConverterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build();

项目地址: https://github.com/qiyi/retrofit-form