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

0 条评论

对于需要访问数据库的 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请求

0 条评论

最近加入到一个新的项目组,项目里基于 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;
}

}

参考:

Windows10下搭建Mysql开发环境

0 条评论

这里的 Mysql 开发环境当然不是指开发 Mysql 程序本身, 只是开发使用 Mysql 的应用,准备个可用的 Mysql 而已。

自从 Windows 10 提供了 bash on ubuntu on windows 特性后,程序员搭建安装一些 linux(恩,知道mysql有windows版本)服务程序就特别方便了。

1. 首先确保 Windows 10 升级到了 Creators Update 或以上,并且启用 Bash on Windows 特性。(Store 里可以选择安装 Linux 发行版了,但是不知道是不是还叫做 bash on ubuntu on windows…)

2. 以安装的 ubuntu 为例, 启动 Bash on ubuntu on windows, 然后执行

sudo apt-get install mysql-server

中间会提示设置 root 密码,并执行

sudo service mysql start

就安装并启动好 mysql server了

3. 创建 database 和用户。

CREATE DATABASE todo CHARACTER SET utf8 COLLATE utf8_general_ci;

指定了字符集为 UTF8, 然后创建用户:

CREATE USER 'todo'@'localhost' identified by 'PASSWORD';

这里指定了用户名为 todo, 只允许通过 localhost 访问。

最后给新建用户赋上权限:

GRANT ALL PRIVILEGES ON todo.* TO todo@localhost;

SpringBoot 集成 Flyway 自动创建数据库表

0 条评论

使用 SpringBoot 开发 Java 应用时,如果涉及到数据库表的创建,可以集成 Flyway,在应用启动时,自动的初始化数据库表。

1. 首先当然是 Maven 工程引入 SpringBoot

参考 https://projects.spring.io/spring-boot/#quick-start 修改工程的 maven 配置,指定父工程,以及添加 spring-boot-starter-web 的依赖.

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2. 引入数据库模块的依赖

SpringBoot内建了大量功能的自动配置能力,这些配置能力大多基于运行时的类检测机制,所以需要引入 spring-jdbc 和 tomcat-jdbc, mysql-connector-java 几个依赖。spring-jdbc 为了启用数据源的自动配置,tomcat-jdbc 为了支持数据库连接池,mysql-connector-java 是添加 mysql 数据库的驱动,依赖如下:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.7-dmr</version>
</dependency>

3. 引入 flyway 的依赖

SpringBoot 的 JDBC AutoConfiguration 已经内建了对 flyway 的支持,唯一要做的,就是添加上 flyway-core 的依赖:

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>4.2.0</version>
</dependency>

4. 配置数据源

SpringBoot 的理念就是约定了大量的默认配置、自动装配能力。所以现在需要配置数据源不需要向以前一样配置各种 spring bean, 而只需要在 resources/application.properties (约定位置和名字,会自动加载)这个配置文件里写上数据库里连接的几个信息:

spring.datasource.url=jdbc:mysql://localhost:3306/todo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=todo
spring.datasource.password=123456

是的,连 driver 信息都不用写,因为 springboot 会自动根据 url 信息去检测。

5. 提供数据库表初始化脚本

还是约定的机制和约定的配置,只需要在工程 resources/db/migration 这个目录下面以 V{version}__{name}.sql 的方式命名数据库脚本文件即可,version 和 name 当然是可变的,举个例子: V1.0__InitializeTables.sql, 注意版本和名字中间是两个下划线。

6. 启动应用自动建表

一切都OK了,确保数据库可连接,用户拥有足够的权限,就可以启动应用,SpringBoot 会自动配置数据源,并使用 Flyway 进行数据库迁移操作。

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

参考: