从Spring的配置文件注册Java Bean在到Spring2.5之后支持注解配置Java Bean,现在SpringBoot也使用Spring6.0来到了3.0版本,当然虽说是来了,但是SpringBoot3.0放弃了Java8所以姜同学还没法再生产环境验证它,所以本文是基于Spring官网的GA版本讲解了。
下面我会结合自己的日常工作介绍我对SpringBoot的理解。也算是我个人的沉淀。
快速开发一个Restful风格的接口
pom文件加入依赖
代码语言:javascript复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
xml
Writing the Code
代码语言:javascript复制@RequestMapping("boot")
@RestController
public class BootController {
@RequestMapping(value = "hello/{name}",method = RequestMethod.GET)
Body home(@PathVariable("name") String name,
@RequestHeader(value = "Auth",required = false) String token,
@RequestParam(value = "param",required = false) String param,
@RequestBody Body body) {
Body response = new Body();
return response.setName(name)
.setToken(token)
.setParam(param);
}
@Data
@Accessors(chain = true)
static class Body{
String name;
String token;
String param;
}
}
java
发起一个Http请求测试一下这个接口 查看配置文件中的端口的应用上下文
我以思维导图的方式讲解一些这些注解的作用
修改请求体中的参数风格
在日常开发中会发现可爱的同事们都习惯于自己的开发风格,有的人喜欢用下划线,有的人喜欢传驼峰风格的参数,所以我们可以在配置文件中添加Boot使用jackson反序列化的风格,这样接口的入参和返回的参数风格就统一啦。
代码语言:javascript复制spring:
jackson:
property-naming-strategy: SNAKE_CASE #这是下划线风格,默认是驼峰的命名风格
default-property-inclusion: non_null
date-format: yyyy-MM-dd HH:mm:ss
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
time-zone: GMT 8
yaml
SpringBoot使用拦截器
实现HandlerInterceptor接口实现自定义拦截器
代码语言:javascript复制@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("目标方法执行前执行,返回true则执行下一个拦截器,没有拦截器直接执行目标方法");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行完毕才会执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("渲染完啦,只要执行过preHandle,及时后面的拦截器失败了也会执行");
}
}
java
注册拦截器
代码语言:javascript复制/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class adminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
java
如果有多个拦截器,你addInterceptor的顺序就是拦截器执行的顺序。
拦截器原理
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】 2、先来顺序执行 所有拦截器的 preHandle方法 ● 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle ● 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion; 3、如果任何一个拦截器返回false。直接跳出不执行目标方法 4、所有拦截器都返回True。执行目标方法 5、倒序执行所有拦截器的postHandle方法。 6、前面的步骤有任何异常都会直接倒序触发 afterCompletion 7、页面成功渲染完成以后,也会倒序触发 afterCompletion
全局异常处理
日常开发中有很多异常是开发人员可以预知的,比如说接口需要的参数格式不对,少了必要参数而引发的NPE,这些异常都是需要捕获返回给前端的,但是大量重复的try catch会使本来很干净的代码变得不那么清爽。所以SpringBoot为我们提供了@ControllerAdvice和@RestControllerAdvice为我们提供捕获全局异常的能力。
代码语言:javascript复制@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 统一处理参数校验的异常
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public Result handleValidException(MethodArgumentNotValidException exception) {
log.error("数据校验出现问题:{},异常类型为:{}", exception.getMessage(), exception.getClass());
BindingResult bindingResult = exception.getBindingResult();
HashMap<String, String> map = new HashMap<>();
// 1.获取校验的错误结果
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
fieldErrors.forEach(item -> {
// 2.获取校验错误的Filed
String field = StrUtil.toUnderlineCase(item.getField());
// 3.获取错误提示
String message = item.getDefaultMessage();
map.put(field, message);
});
return Result.result(CODE_MSG.PARAM_EXCEPTION,map);
}
/**
* 捕获自定义异常
*/
@ExceptionHandler({PinsRuntimeException.class})
@ResponseStatus(code = HttpStatus.OK)
public Result<Void> handleMyException(Exception ex) {
log.error("PinsRuntimeException error:{}", ex.getMessage());
return Result.fail(ex.getMessage());
}
}
java
这样我们只需要在代码中抛出异常就ok啦。
SpringBoot多环境配置
Spring多环境的配置也是非常简单的,你只需要准备下面四个文件放在resources文件夹下即可。
然后在不同的文件中使用下面的配置指定自己的环境即可,以dev为例。
代码语言:javascript复制spring:
config:
activate:
on-profile: dev
yaml
最后在application.yml声明使用哪个环境即可。
代码语言:javascript复制spring:
profiles:
active: "dev"
yaml
Bean的自动装配
基于Spring的IOC原理,Bean的自动注入让我们完全感受不要创建对象的过程,就感觉bean不需要创建一样,那么我们怎么将我们自定义的对象注册到IOC容器呢,当然也很简单,不过我最常用的还是下面这两种。
下面演示一下我最常用的@Bean方式。
代码语言:javascript复制@Configuration
public class ThreadPoolConfig {
@Bean
public ExecutorService pinsTaskThreadPool(){
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1000);
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("pins-thread-%s").build();
int availableProcessors = Runtime.getRuntime().availableProcessors();
// IO密集型线程池
int maximumPoolSize = 2*availableProcessors;
return new ThreadPoolExecutor(maximumPoolSize-1, maximumPoolSize,
10, TimeUnit.SECONDS, workQueue,
namedThreadFactory,
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RejectedExecutionException("Queue full...");
}
});
}
}
java
这样我们自定义的线程池就交给Spring的容器管理啦。
Bean的条件装配
有一些Bean是要互相依赖滴,比如SpringBoot的DataSourceHealthContributorAutoConfiguration就需要依赖一些其他的Bean,让我们来看看SpringBoot是怎么做的。
上面注解的意思是加载到JdbcTemplate,AbstractRoutingDataSource,两个类并且容器中有DataSource类型的Bean这个配置类才会生效。下面我继续以思维导图的方式介绍一些条件装配常用的注解。
控制Bean到加载顺序
姜同学之前写过一个redis的SDK,过段时间一起分享出来,这里并不是要强调这个SDK,而是要描述一下项目中引用这个SDK发生的问题,业务系统使用的认证授权框架是开源的sa-token,里面集成了redis,我进去改人家的源代码就有点用大炮打蚊子的感觉了,那怎么才能让这些第三方框架都使用姜同学开发的SDK的配置呢,请听我娓娓道来。 首先姜同学发现了sa-token的redis配置类。
类上面的@Component,暴露的它是IOC容器中的一个Bean,而且这个框架很讲道理呀RedisTemplate,声名成了public,怎么让它使用我的SDK这个问题就很好解决了,我们只需要在创建一个Bean让他在sa-token的redis配置bean之后加载,并在加载之前把RedisTemplate替换为我们redis SDK的RedisTemplate就好啦,这时我们的主角@DependsOn,就登场了,这个注解约束指定Bean存在才会创建注解标注的Bean。
代码语言:javascript复制@Component
@DependsOn(value = {"cn.dev33.satoken.dao.SaTokenDao"})
public class SaTokenConfigure{
private final AuthProperties authProperties;
private final PinsStandaloneRedis pinsStandaloneRedis;
private final ApplicationContext applicationContext;
public SaTokenConfigure(AuthProperties authProperties, PinsStandaloneRedis pinsStandaloneRedis, ApplicationContext applicationContext) {
this.authProperties = authProperties;
this.pinsStandaloneRedis = pinsStandaloneRedis;
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
RedisConnectionFactory connectionFactory = pinsStandaloneRedis.getRedisTemplate().getConnectionFactory();
String[] beanNamesForType = applicationContext.getBeanNamesForType(SaTokenDaoRedisJackson.class);
SaTokenDaoRedisJackson saTokenDaoRedisJackson = (SaTokenDaoRedisJackson) applicationContext.getBean(beanNamesForType[0]);
assert connectionFactory != null;
saTokenDaoRedisJackson.objectRedisTemplate.setConnectionFactory(connectionFactory);
saTokenDaoRedisJackson.stringRedisTemplate.setConnectionFactory(connectionFactory);
}
}
java
在init方法中替换了sa-token的redis配置,这都多亏了DependsOn注解呢。
修改已经装配的Bean
其实在上一步我们已经演示了如何修改已经加载的Bean。
- 自动注入ApplicationContext类
- getBean获取已经加载的Bean
- 拿到Bean修改属性即可 当然修改方法不止我这一种哦。
当然还有其他一些好用的API,都很简单,姜同学就留给大家自己探索了。
开发自定义的boot-start
打开项目的pom文件你会看到很多类似的boot-starter,它们到底是什么东西呢?姜同学以自己写过的一个阿里云RocketMQ的SDK为例讲解下这个boot-starter究竟是个什么东西。 先揭晓一下答案,其实boot-starter里面就是定义了一些我们可以直接使用的JavaBean.
创建boot项目,定义MQ组件.
代码语言:javascript复制@Component
@Qualifier("pinsNormalRocketMQ")
public class PinsNormalRocketMQ implements PinsMQ{
private final ProducerBean pinsNormalProducer;
private final MqConfig mqConfig;
private final ExecutorService executorService;
private static HashSet<ConsumerBean> consumerSet = new HashSet<ConsumerBean>();
public PinsNormalRocketMQ(ProducerBean pinsNormalProducer, MqConfig mqConfig) {
this.pinsNormalProducer = pinsNormalProducer;
this.mqConfig = mqConfig;
this.executorService = ThreadPoolFactory.buildThreadPool();
}
@Override
public void send(String tag, String key,@NotNull String msg) {
Message message = new Message(
mqConfig.getTopic(),
tag,
msg.getBytes(StandardCharsets.UTF_8));
message.setKey(key);
pinsNormalProducer.send(message);
executorService.execute(() ->
PinsMessageUtils.saveRecord(mqConfig.getEnv(), RocketMqRecordBuilder.buildProductRecord(message)));
}
}
java
可以看到我使用了@Component这个注解将PinsNormalRocketMQ声明为IOC容器管理的JavaBean,并用@Qualifier为这个Bean起了一个名字。
创建spring.factories文件
SpringBoot为我们提供了一种机制,那就是在resources/META-INF/目录下创建一个spring.factories文件,并在里面写上我们想要自动装配的Bean,SpringBoot在启动时就会加载到这个文件,并把里面的Bean注册到容器中。
这就是他的目录结构。 里面的内容是这样滴。
代码语言:javascript复制org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.pinsmedical.mq.core.PinsNormalRocketMQ,
com.pinsmedical.mq.normal.ConsumerClient,
com.pinsmedical.mq.normal.ProducerClient,
com.pinsmedical.mq.normal.DemoMessageListener,
com.pinsmedical.mq.core.PinsTransactionRocketMQ,
com.pinsmedical.mq.transaction.TransactionProducerClient,
com.pinsmedical.mq.transaction.PinsLocalTransactionChecker
java
可以看到我们上面的com.pinsmedical.mq.core.PinsNormalRocketMQ就在其中,那么下面那些东西是什么鬼呢,姜同学在这个工程中创建了很多Bean,为了演示只是选取了PinsNormalRocketMQ这个类复制了出来,大家知道一下就好啦。
打包传入maven私服
先看一下我们这个工程的gav,如果你没有在maven的配置文件中配置私服地址,那么你以可以直接mvn install将jar直接安装到你的本地仓库。
引入依赖
代码语言:javascript复制<dependency>
<groupId>com.pinsmedical</groupId>
<artifactId>pins-mq</artifactId>
<version>2.1-RELEASE</version>
</dependency>
xml