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 

使用 MariaDB4j 给 Spring Boot 应用做集成测试

2 条评论

对于需要访问数据库的 Java 应用来说,集成测试一般都不太方便,一个项目里如果团队共享一个数据库的话,大家执行用例肯定互相干扰,而如果让每个成员本地自行搭建数据库,维护起来也很麻烦,跑用例或者构建前要去准备数据库,实在不是好的开发体验。还有如 MyBatis 推荐的那样使用内存数据库来做自动化的用例,但是可能会有潜在的不同数据库之间的差异无法被验证到,和生产环境数据库毕竟不同。

目前公司项目组使用 MySQL,个人项目也在使用 MySQL, 所以最近找了下怎么做数据库相关的集成测试的资料,发现一个 MariaDB4j 的项目,把 MariaDB 封装得像是一个 Java 的嵌入式数据库了,这就非常便于 Java 应用在代码里直接控制数据库的起停、创建。在这个基础上,又基于 Spring Boot 的 AutoConfiguration 机制做了一些封装,使用起来非常方便,介绍下实现。

1. 引入 Spring Boot 相关测试依赖、Junit5 Spring 扩展依赖、MariaDB4j 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.3.11.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test-autoconfigure</artifactId>
</dependency>
<dependency>
    <groupId>com.github.sbrannen</groupId>
    <artifactId>spring-test-junit5</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>ch.vorburger.mariaDB4j</groupId>
    <artifactId>mariaDB4j</artifactId>
    <version>2.2.3</version>
    <scope>test</scope>
</dependency>

2. 自定义 Spring 注解

为了简化在 Spring Boot 应用下的使用,所以基于类似 Spring Boot 的 Auto Configure 机制做了封装,达到只需要在测试类上添加一个注解,就可以自行启动一个 MariaDB 数据库,并创建指定的 database 的目的。

首先创建一个 MariaDB4jFactoryBean,继承了 MariaDB4j 项目里的 MariaDB4jSpringService,这个 Bean 根据配置信息在启动数据库后创建指定的 database,然后实现一个注解 AutoConfigureMariaDB,处理此注解的类为 TestMariaDBAutoConfiguration, TestMariaDBAutoConfiguration 是一个 Spring 的 Java Configuration 类,在这里暴露 MariaDB4jFactoryBean。最后在 META-INF/spring.factories 里把注解和类实现给添加进去,这样 Spring Boot 初始化的时候,就会根据注解找到对应的 Configuration 类,并自动暴露添加的 Spring Bean 了,并且 Auto Configuration 还可以指定配置的顺序,这里创建数据库就指定在 DataSourceAutoConfiguration 之前了。具体的代码实现可以参考: https://github.com/momoment/pink/tree/master/src/test/java/com/momo/pink/test/autoconfigure

3. 写一个集成测试用例

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = PinkApp.class, properties = {
    "spring.test.maria.database=pink",
    "spring.datasource.username=root",
    "spring.datasource.password=",
}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMariaDB
public class UserTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testCreateUser() {
        User user = this.restTemplate.postForObject(
            "/api/v1.0/users", new User()
                .setEmail("bphanzhu@gmail.com")
                .setName("qiyi"), User.class);
        assertNotNull(user.getId());
        User queryUser = this.restTemplate.getForObject(
            "/api/v1.0/users/" + user.getName(), User.class);
        assertEquals(user.getId(), queryUser.getId());
        assertEquals(user.getName(), queryUser.getName());
        assertEquals(user.getEmail(), queryUser.getEmail());
    }
}

使用了 Junit5 的 ExtendWith 来启动 SpringBoot 应用,SpringBootTest 注解指定了应用入口类,以及测试环境下的一些自定义配置,同时要求真实地启动 tomcat 环境,最后一个 @AutoConfigureMariaDB 注解就直接把数据库给创建起来了,配合 Flyway 的集成可以自动完成数据库表的创建。

SpringWeb基于请求参数值分发HTTP请求

1 条评论

最近加入到一个新的项目组,项目里基于 Spring Web 做 HTTP(不敢说是 REST)请求的接入,由于一些历史原因,请求的 URI 都规划成了一样的,比如只有三个请求 URI: /client/api, /server/api, /wap/api。但是规划请求里总会带一个参数 method, 不同业务请求的 method 值是不一样的,服务端不同的 Controller 处理不同 method 的 HTTP 请求。

当前的实现继承了 Spring 的 AbstractUrlHandlerMapping 类,然后使用 XML 配置的方式声明成一个 Bean,并且给 Bean 注入初始化的 map 类型的配置: key是不同的 method, value 是不同的 Controller 实现。Bean 在初始化的时候,还会把 method 名字当作 url 注册到 AbstractUrlHandlerMapping 里去,最主要的是重写了 getHandlerInternal 方法,方法的实现就是取请求的 method 参数值,然后根据初始化 map 找到对应的 Controller Bean 并返回。 这里还会额外约束 Controller 的实现必须继承 Spring 的 MultiActionController (其实继承 AbstractController 类应该就OK了)

不过感觉当前的实现方式有些复杂,一是继承了 AbstractUrlHandlerMapping,但其实需要的路由分发逻辑和 Url 是没有关系的,分发逻辑是非常简单的根据 method 请求参数的值,找到映射的 Controller 即可,即使要重写映射逻辑,继承 AbstractHandlerMapping 也就够了。另外,必须使用 xml 进行配置,缺少了现在流行好用的 Java Config 的支持,不利于编译器检查错误和重构。最后还要求实现者必须继承 MultiActionController 类,强制使用原始的 HttpServletRequest 对象,导致业务代码里大量和业务关联不大的取值代码,字符串常量定义,效率低还容易出错。

其实 Spring 的 RequestMapping 注解本身就有提供对参数进行判断映射的能力,使用其 params 属性就可以。样例如下:

第一个 controller 在类上添加 requestMapping 注解,指定 path 是 /e (固定值),然后 params 指定 method=e1

@Controller
@RequestMapping(value = "/e", params = "method=e1")
public class E1 {
    @PostMapping("")
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.getWriter().write("e1");
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

第二个 controller 也是在类上指定 requestMapping 的 path 是 /e(固定值),然后 params 指定为 method=e2

@Controller
@RequestMapping(value = "/e", params = "method=e2")
public class E2 {
    @PostMapping("")
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.getWriter().write("e2");
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

这样一来,不用额外实现 AbstractHandlerMapping 类,我们、、完全可以和普通基于 URI Path 进行路由分发的场景一样来实现 controller,controller 路由信息也由 Spring 自行从注解信息上获取, controller 的实现不会要求强制继承某个类,可以充分使用 spring 提供的注解来标记方法参数和 http 请求参数的映射让 spring 自动提取转换。

测试结果:

spring-mapping-by-param

不管是 query 请求参数,还是 form data 里的参数, spring 都能正确处理。

SpringBoot 集成 Mybatis

0 条评论

跟着前面 SpringBoot 集成 Flyway 自动创建数据库表 之后,在原有的项目上继续集成 Mybatis 做数据库的访问、对象映射。

1. 继续添加 mybatis 的 spring boot starter 包

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

这个依赖包主要引入了 mybatis-spring-boot-autoconfigure,利用了 spring boot 的 auto configure 机制,可以自动从 spring boot 里获取数据源,自动创建 SqlSessionFactory 等实例,所以有了它之后, mybatis 的什么 spring 配置都不用再管了。

2. 添加 Mapper 扫描的注解

这里使用注解的方式来使用 mybatis 的 mapper,由于没有 mybatis 配置文件了,就在 SprintBoot 应用的类上添加 @MapperScan 注解

@SpringBootApplication
@MapperScan("org.isouth.task")
public class TaskApp {
public static void main(String[] args) {
SpringApplication.run(TaskApp.class, args);
}
}

然后要求作为 Mybatis 的 Mapper 接口声明 @Mapper 注解:

@Mapper
public interface UserMapper {
@Select("SELECT * FROM USERS")
List<User> listUsers();

@Select("SELECT * FROM USERS WHERE EMAIL=#{email}")
User getUser(String email);

@Insert("INSERT INTO USERS (EMAIL, ALIAS) VALUES(#{email},#{alias})")
void addUser(User user);

@Delete("DELETE FROM USERS WHERE EMAIL=#{email}")
void deleteUser(String email);
}

3. 直接装配引用 Mapper接口

然后可以在 Controller 或者 Service 里直接使用 @Autowire 来装配 Mapper 接口,并直接调用接口方法使用了。

@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserMapper userMapper;

@RequestMapping(method = RequestMethod.POST, path = "")
@ResponseBody
public User addUser(@RequestBody User user) {
userMapper.addUser(user);
return user;
}

}

参考: