本文和大家分享一下,Spring Boot的一些小技巧,知道这些小技巧,有助于你在实际项目中解决一些特殊的需求。
技巧一:调整 bean 的加载顺序
调整 bean 的加载顺序的方式有很多种:
1、可以通过 @Autowired 的方式,让一个 bean 进行初始化
例如: ServiceA 加载之前,一定要初始化 ServiceB,则可以将 ServiceB 通过 @Autowired 时行注入,让 ServiceB 在 ServiceA 之前时行初始化
2、通过注解@ConditionalOnBean(xxx) 说明这两个Bean的依赖关系。
技巧二:classloader.getResource() 拿不到资源
在特定的环境下:A包里面有resources/res.txt资源
在B包中引用了A包,在获取res.txt资源时就会报错,拿不到资源
使用 IDEA 运行 springboot 程序与 java -jar 运行 springboot 程序时 ClassLoader 不同,导致 classloader.getResource() 拿不到资源
使用 this.getClass().getClassLoader() 获取 classloader 时,运行方式不同,结果不一样
A、使用 IDEA 运行 springboot 程序时,sun.misc.Launcher$AppClassLoader@18b4aac2
B、使用 java -jar 运行打包后的 jar 包时,org.springframework.boot.loader.LaunchedURLClassLoader@71dac704
原因在于:在Jar包中的文件在磁盘是没有实际路径的,所以这时候通过 this.getClass().getResource() 无法获取文件。
对于原始的JarFile URL,只支持一个’!/’,SpringBoot 扩展了此协议,使其支持多个’!/’,以实现 jar in jar 的加载资源方式。
但是,取到了资源路径,原生的 new File() 还是处理不了这种资源路径的。因为里面带多个!/路径,正常是无法获取到资源的。会报错提示:
Caused by: java.io.FileNotFoundException: class path resource [res.txt] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/Users/kyle/CodeJava/qyun-web/qyun-system-api/target/qyun-system-api.jar!/BOOT-INF/lib/qyun-component-mybatis-1.0.jar!/res.txt
测试下面的办法是取不到路径的:
String resourcePath = "res.txt";
String path = this.getClass().getResource("/").getPath() + resourcePath;
URL fileURL = this.getClass().getResource(resourcePath);
我们通过传统的方式取不到资源了,即使取到了,也没法直接使用,那在 SpringBoot 中我们应该怎么获取资源呢?
不太优雅的方案:
private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
public org.springframework.core.io.Resource[] resolveMapperLocations(String[] mapperLocations) {
return Stream.of(Optional.ofNullable(mapperLocations).orElse(new String[0]))
.flatMap(location -> Stream.of(getResources(location))).toArray(org.springframework.core.io.Resource[]::new);
}
private org.springframework.core.io.Resource[] getResources(String location) {
try {
return resourceResolver.getResources(location);
} catch (IOException e) {
return new org.springframework.core.io.Resource[0];
}
}
从路径中解析出资源,读取时,可以直接用
res.getContentAsString(StandardCharsets.UTF_8)
最后再提供一个最简方案,使用Hutool工具库 https://github.com/dromara/hutool
URL file4 = ResourceUtil.getResource(fileName);
System.out.println("Test 4");
System.out.println(Objects.nonNull(file4) ? FileUtils.readFileToString(new File(file4.getFile()), StandardCharsets.UTF_8) : "test 4 file is null");
技巧三:排除自动配置的几种方式
1、指定class排除
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
2、指定类名
@SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration")
3、配置文件配置
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
技巧四: @ConditionalOnProperty 根据配置加载不同的 bean
场景:对 redis 配置进行封装,实现自动化配置,能兼容哨兵模式和集群模式。
想到在 redis 配置中加一个 redis.type 来区分集群和哨兵模式(redis.type=cluster 或 sentinel),然后根据 type 来分别加载 JedisConnectionFactory、RedisClusterConfiguration、RedisSentinelConfiguration
配置如下:
@ConditionalOnProperty(name = "redis.type", havingValue = "cluster")
@ConditionalOnMissingBean
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
}
@ConditionalOnProperty(name = "redis.type", havingValue = "sentinel")
@ConditionalOnMissingBean
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
Set<String> sentinelHostAndPorts = Sets.newHashSet(Splitter.on(",").split(hostName).iterator());
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(masterName, sentinelHostAndPorts);
return redisSentinelConfiguration;
}
技巧五: 实现用户行为记录的四种方式
根据产品经理要求,需要对用户的行为进行记录。重点记录用户的姓名、IP、操作行为、请求参数和返回参数。先采用暴力解决问题,每个行为增加记录行为,当然这个是最不恰当的方案,工作量大不说,还不具有扩展性。有4种方案来实现该需求:
1、AOP切面编程实现:在每一个需要记录日志的方法前增加切点,然后对切点进行处理即可。
2、Interceptor拦截器实现:定义拦截器类 OperateLogInterceptor 类并实现 HandlerInterceptor 接口。
3、Filter过滤器实现
4、ArgumentResolver:参数解析器是 Spring 提供的用于解析自定义参数的工具,使用它,我们可以将参数在进入Controller Action之前就组合成我们想要的样子。
技巧六: 测试中使用随机HTTP端口
在Spring Boot测试中不应该使用静态端口,为了对指定的测试设置该选项,你需要设置 @SpringBootTest 注解中的 webEnvironment 字段,将它的指定为 RANDOM_PORT 而不是默认的 DEFINED_PORT 。配置完后你可以使用 @LocalServerPort 注解将这个随机生成的端口号注入到测试类中。
比如:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartUpApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
技巧七:使用@DataJpaTest来测试JPA接口
对于集成测试,通常情况下你可能会使用 @SpringBootTest 来注释测试类,这样做的问题在于它启动了整个应用程序上下文,这会增加运行测试所需的总时间。更好的选择是:你可以使用 @DataJpaTest 来启动JPA组件和带有 @Repository 注解的bean。默认情况下它会在日志中记录SQL查询语句,因此一个好主意是使用 showSql 字段禁用这个特性。此外,如果你希望将带有 @Service 或 @Component 注解的bean包含到测试中,可以使用 @Import 注解。