0%

日常工作中对于Spring Boot 提供的一些启动器可能已经足够使用了,但是不可避免的需要自定义启动器,比如整合一个陌生的组件,也想要达到开箱即用的效果。

这篇文章将会介绍如何自定义一个启动器,同时对于自动配置类的执行顺序做一个详细的分析。

阅读全文 »

前言

为什么Spring Boot这么火?因为便捷,开箱即用,但是你思考过为什么会这么便捷吗?传统的SSM架构配置文件至少要写半天,而使用Spring Boot之后只需要引入一个starter之后就能直接使用,why???

原因很简单,每个starter内部做了工作,比如Mybatis的启动器默认内置了可用的SqlSessionFactory

至于如何内置的?Spring Boot 又是如何使其生效的?这篇文章就从源码角度介绍一下Spring Boot的自动配置原理

阅读全文 »

从哪入手?

相信很多人尝试读过 Spring Boot 的源码,但是始终没有找到合适的方法。那是因为你对 Spring Boot 的各个组件、机制不是很了解,研究起来就像大海捞针。

至于从哪入手不是很简单的问题吗,当然主启动类了,即是标注着 @SpringBootApplication 注解并且有着 main() 方法的类,如下一段代码:

1
2
3
4
5
6
7
@SpringBootApplication
public class BootApplication {

public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
阅读全文 »

前言

注解相信大家都用过,尤其是 Spring Boot 这个框架,比如 @Controller

这篇文章就来介绍下 Spring Boot 中如何自定义一个注解,顺带介绍一下 Spring Boot 与 AOP 如何整合。

阅读全文 »

前言

日常开发中至少有三个环境,分别是开发环境( dev ),测试环境( test ),生产环境( prod )。不同的环境的各种配置都不相同,比如数据库,端口, IP 地址等信息。

阅读全文 »

什么是 JSR-303?

JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation 。

Bean Validation 为 JavaBean 验证定义了相应的 元数据模型 和 APT。缺省的元数据是 Java Annotations ,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint ,例如@NotNull, @Max, @ZipCode,就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint 。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

阅读全文 »

Testcontainers for Java简介

Java的Testcontainers是一个支持JUnit测试的Java库,提供通用数据库,Selenium Web浏览器或任何其他可以在Docker容器中运行的轻量级一次性实例。

Testcontainers 能够让你实现通过编程语言去启动Docker容器,并在程序测试结束后,自动关闭容器。这基本上能解决我们大部分的需求。

使用 TestContainers 这种解决方案还有以下几个优点:

  • 每个Test Group都能像写单元测试那样细粒度地写集成测试,保证每个集成单元的高测试覆盖率。
  • Test Group间是做到依赖隔离的,也就是说它们不共享任何一个Docker容器;假如两个Test Group都要用到Mongo 4.0,会创建两个容器供它们单独使用 。
  • 保证了生产环境和测试环境的一致性,代码部署到线上时不会遇到因为依赖服务接口不兼容而导致的bug 。
  • Test Group可以并行化运行,减少整体测试运行时间。相比较有些 in-memory 的依赖服务实现没有实现很好的资源隔离,比如端口,一旦并行化运行就会出现端口冲突 。
  • 得益于Docker,所有测试都可以在本地环境和 CI/CD环境中运行,测试代码调试和编写就如同写单元测试。

测试容器使以下类型的测试更容易:

  • 数据访问层集成测试:使用 MySQL、PostgreSQL 或 Oracle 数据库的容器化实例来测试数据访问层代码的完全兼容性,但不需要在开发人员的计算机上进行复杂的设置,并且知道您的测试将始终从已知的数据库状态开始。也可以使用任何其他可以容器化的数据库类型。
  • 应用程序集成测试:用于在具有依赖项(如数据库、消息队列或 Web 服务器)的短期测试模式下运行应用程序。
  • UI/验收测试:使用与Selenium兼容的容器化Web浏览器进行自动化UI测试。每个测试都可以获得浏览器的新实例,无需担心浏览器状态、插件变体或自动浏览器升级。您可以获得每个测试会话的视频记录,或者只是测试失败的每个会话。

先决条件

Maven 依赖项

对于核心库,最新的 Maven/Gradle 依赖项如下:

1
2
3
4
5
6
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>

管理多个测试容器依赖项的版本

若要避免指定每个依赖项的版本,可以使用BOMBill Of Materials

使用 Maven,您可以将以下dependencyManagement添加到您的pom.xml

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.18.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后在不指定版本的情况下使用依赖项:

1
2
3
4
5
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>

JUnit 4 快速入门

假设我们有一个简单的程序依赖于 Redis,我们想为它添加一些测试。 在我们的虚构程序中,有一个类在 Redis 中存储数据。

RedisBackedCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface Cache {

void put(String key, Object value);

<T> Optional<T> get(String key, Class<T> expectedClass);
}

public class RedisBackedCache implements Cache {

private final Jedis jedis;

private final String cacheName;

private final Gson gson;

public RedisBackedCache(Jedis jedis, String cacheName) {
this.jedis = jedis;
this.cacheName = cacheName;
this.gson = new Gson();
}

@Override
public void put(String key, Object value) {
String jsonValue = gson.toJson(value);
this.jedis.hset(this.cacheName, key, jsonValue);
}

@Override
public <T> Optional<T> get(String key, Class<T> expectedClass) {
String foundJson = this.jedis.hget(this.cacheName, key);

if (foundJson == null) {
return Optional.empty();
}

return Optional.of(gson.fromJson(foundJson, expectedClass));
}
}

Maven依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

您可以看到一个可能为其编写的示例测试(不使用 Testcontainers):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RedisBackedCacheIntTestStep0 {

private RedisBackedCache underTest;

@Before
public void setUp() throws Exception {
Jedis jedis = new Jedis("localhost", 6379);

underTest = new RedisBackedCache(jedis, "test");
}

@Test
public void testSimplePutAndGet() {
underTest.put("test", "example");

String retrieved = underTest.get("test");
assertThat(retrieved).isEqualTo("example");
}
}

请注意,现有测试存在问题 - 它依赖于 Redis 的本地安装,这是测试可靠性的危险信号。 如果我们确定每个开发人员和 CI 计算机都安装了 Redis,这可能会起作用,否则会失败。 如果我们尝试并行运行测试,我们也可能会遇到问题,例如测试之间的状态出错或端口冲突。

使用 Testcontainers 改进测试:

让测试容器在我们的测试期间运行 Redis 容器

JUnit 4 规则

1
2
3
4
@Rule
public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:7.0.5"))
.withExposedPorts(6379);

@Rule注释告诉 JUnit 通知此字段有关测试生命周期中的各种事件。 在这种情况下,我们的规则对象是一个 测试容器GenericContainer ,配置为使用 Docker Hub 中的特定 Redis 映像,并配置为公开端口。

如果我们按原样运行测试,那么无论实际测试结果如何,我们都会看到日志,显示 Testcontainers:

  • 在我们的测试方法运行之前被激活
  • 发现并快速测试了我们的本地 Docker 设置
  • 如有必要,拉取图像
  • 启动一个新容器并等待它准备就绪
  • 测试后关闭并删除容器

确保我们的代码可以与容器通信

在 Testcontainers 之前,我们可能已经将一个地址硬编码到我们的测试中。localhost:6379

Testcontainers 对其启动的每个容器使用随机端口,但可以轻松地在运行时获取实际端口。 我们可以在测试方法中执行此操作,以设置被测试组件:setUp

获取映射端口

1
2
3
4
5
6
String address = redis.getHost();
Integer port = redis.getFirstMappedPort();

// Now we have an address and port for Redis, no matter where it is running
underTest = new RedisBackedCache(address, port);

运行测试!

RedisBackedCacheIntTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RedisBackedCacheTest {

@Rule
public GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0.5"))
.withExposedPorts(6379);
private Cache cache;

@Before
public void setUp() throws Exception {
Jedis jedis = new Jedis(redis.getHost(), redis.getMappedPort(6379));
cache = new RedisBackedCache(jedis, "test");
}
@Test
public void testFindingAnInsertedValue() {
cache.put("foo", "FOO");
Optional<String> foundObject = cache.get("foo", String.class);

assertThat(foundObject.isPresent()).as("When an object in the cache is retrieved, it can be found").isTrue();
assertThat(foundObject.get())
.as("When we put a String in to the cache and retrieve it, the value is the same")
.isEqualTo("FOO");
}
}

什么是多数据源?

最常见的单一应用中最多涉及到一个数据库,即是一个数据源( Datasource )。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库了。

阅读全文 »

@Conditional注解是从 Spring4.0 才有的,可以用在任何类型或者方法上面,通过@Conditional注解可 以配置一些条件判断,当所有条件都满足的时候,@Conditional标注的目标才会被Spring容器处理。

@Conditional的使用很广,比如控制某个 Bean 是否需要注册,在Spring Boot中的变形很多,比如 @ConditionalOnMissingBean@ConditionalOnBean 等等

阅读全文 »

自从用了Spring Boot是否有一个感觉,以前MVC的配置都很少用到了,比如视图解析器,拦截器,过滤器等等,这也正是Spring Boot开箱即用的好处。

但是往往Spring Boot提供默认的配置不一定适合实际的需求,因此需要能够定制MVC的相关功能,这篇文章就介绍一下如何扩展和全面接管MVC。

阅读全文 »