Low level test

1 条评论

开发人员最为熟悉的测试手段是单元测试,单元测试通常只关注单个类、单个方法的功能,(java)在写作单元测试用例时一般基于 Junit 框架使用 EasyMock/PowerMock 等为被测试类的依赖打桩,可以说是非常方便了,不过单元测试无法验证系统或者模块在一个业务流程上的处理逻辑是否符合预期,为了验证当前系统或者模块对一整个业务流程的处理,我们就需要做集成测试了。集成测试时,我们不再只初始化单个的类,而是按照系统运行要求启动整个模块或者整个系统,然后模拟其他系统来和当前系统进行交互,并对运行结果进行检查。很明显,让一个系统完整地运行起来,需要满足非常多的外部条件,比如Web容器、数据库、缓存、消息队列、其他外部依赖服务等,想要满足这么多的条件非常不易,更何况自动化的测试用例还需要尽可能地确保用例之间的隔离,用例不同执行者之间的隔离(如果共享公共的外部环境依赖,很容易不同人在执行用例时会互相干扰)。因为集成测试外部条件的准备不易,很多项目只能放弃这一测试手段,而期待测试团队的验证。

足够多的自动化测试用例是提升产品质量的有效保证,是减少问题、减少加班性价比非常高的方式,而让开发人员自主就能完成用例编写又能极大降低问题发现、处理的成本,所以在条件允许的情况下,开发人员来编写的单元测试、集成测试用例是非常有必要的。那么集成测试是否可以参考单元测试的EasyMock等框架,为很多通用的外部环境依赖提供简单、好用的Mock能力呢?我尝试为这个事情开个坑,目标是在集成测试里成为类似于单元测试里EasyMock的存在,为大部分应用提供可以通过API进行控制的Web运行环境、数据库依赖、etcd依赖、redis依赖、cassandra依赖、kafka依赖等,让开发人员可以很低成本地开发集成测试用例,坑在这里:https://github.com/qiyi/llt ,后续再慢慢介绍不同外部环境依赖的使用方式。

根据条件过滤日志输出

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();