导语
在之前的一篇中简单的介绍一些在SpringBoot中常见的使用方式,已经如何实现自定义的一些配置,设置等等的内容,这一次的分享中来介绍一些在Spring Boot 中的高级用法,这样会对Spring Boot的整个的使用场景,有更加深刻的认识。
统一异常处理
现在REST Ful 风格的兴起对于接口的定义,通常会有一些统一的格式,这样的话在调用方与实现方之间有一个统一的规则如下所示
{
"status":true,
"code":200,
"msg":"numal",
"data":{
"name":"nihui",
"password":"adjfakdfja;lkdjfla;kds"
}
}
但是如果在调用接口的时候,出现异常或者是系统内部出现其他异常的时候,会得到一个错误的提示信息
{
"timestamp":"456456456456",
"status":500,
"error":"System Error",
"message":"No message available",
"path":"/hello/test"
}
表示后端服务器一定是出现什么问题了,这样其实也还是很好的,但是这样会导致与上面自定义的返回数据不一致,在接口的使用上会带来一定程度上的不便,这个时候就需要在服务端编写接口的时候将这个因素考虑进去,如果出现了异常怎么实现。
这样Spring Boot就提供了一个全局的异常处理机制。这样无论在什么情况下都可以准确的返回统一格式的数据信息。如下操作
@ControllerAdvice
public class GlobelExecptionHandler{
private Logger logger = LoggerFactory.getLogger(GlobelExceptionHandler.class)
@ExceptiongHandler(value=Exception.class)
@ResponseBody
public AjaxResult defalueErrorHandler(HttpServletRequest req,Exception e) throw Exception{
logger.error("",e)
AjaxResult result = new AjaxResult();
result.setMessage(e.getMessage())
if(e instanceof Exception){
result.setCode(500)
}
result.setData(null);
result.setStatus(false);
return result
}
}
其中AjaxResult 是返回格式的实体类,在发生异常的时候被捕捉到之后,会将封装好的数据返回到调用方,但是只到这里是不够的。还要在配置文件中加入如下的配置才可以
# 出现错误的时候直接抛出异常
spring.mvc.throw-exceptioin-if-no-handler-found=true
# 不要为工程中的资源文件建立映射
spring.resource.add-mappings=false
当然上面介绍的只是一种简单的实现方式,但是这个是全局的异常处理,包括自定义异常处理提供了一个解决思路。其他的高级的扩展思路都是从最简单的扩展思路上进行操作的,这个也就是所谓的万变不离其宗。实现的原理掌握了,怎么实现就由大家来决定呢?
异步执行
异步的意思就是不需要等待处理结果的返回就可以执行后面的逻辑;而同步的意思则是需要等待结果的然后再执行后续的逻辑,通常情况下使用异步操作的时候都会创建一个线程执行一段逻辑,然后把线程放入到线程池中去执行,例如
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(()->{
try{
//业务逻辑
}cache(Exception e){
e.printStackTrace();
}finally{
}
})
上面这种方式使用了Lambda表达式,但是整个的执行逻辑还是比较繁琐的,而在Spring中提供了一个@Async 的注解,通过这个注解可以实现方法的异步调用。
@Async
public void saveLog(){
System.out.println(Thread.currentThread().getName());
}
通过这种方式可以直接在Controller中调用该方法,这个方法就是异步进行执行的,这样它会在Spring的默认线程池中执行。但是需要注意的一点就是,一定要在外部类中调用,在本类中调用它是不起作用的。
既然说到了执行异步操作,那就不得不提线程池,在Spring Boot通过如下的方式进行线程池的配置
@Configuration
@ConfigurationProperties(prefix="spring.task.pool")
public class TaskThreadPoolConfig{
// 核心线程数
private int corePoolSize=5;
// 最大线程数
private int maxPoolSize=50;
// 线程中维护线程所需要的空闲时间
private int keepAliveSeconds = 60;
// 队列长度
private int queueCapacity = 1000;
// 线程前缀名
private String threadNamePrefix = "SPRING-"
}
定义好配置参数之后,接下来就是重新定义线程池配置类
@Configuration
public class AsyncTaskExecutePool implements AsyncConfigurer{
private Logger logger = LoggerFactory.getLogger(AsyncTaskExecutePool.class);
@Autowired
private TaskThreadPoolConfig config;
@Override
public Executor getAsyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(config.getCorePoolSize());
………………
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughExceptionHandler getAsyncUncaughtExceptionHandler(){
return new AsyncUncaughtExceptionHandler(){
@Override
public void handlerUncaughtException(Throwable arg0,Method,arg1,Object arg2){
……
}
}
}
}
配置完成之后,整个的异步线程池就是自定义的了,可以在其中配置上面提到的内容。但是其中有一个比较关键的点,就是线程的拒绝策略。要知道在线程池外部等待队列空间是有限的,当这个等待队列满了之后,如果还有线程加入的话就需要支持一种线程拒绝策略,否则就会导致内存溢出的后果。目前支持的两种拒绝策略
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:主线程直接执行该任务,之心完成之后尝试添加下一个任务线程到线程池中,这样可以对线程池有一个保护的作用。
推荐使用CallerRunsPolicy策略,当队列中任务满了之后,如果执行抛出异常那么这个任务将会被丢失。使用这个策略可以保证该任务不会被丢失。
端口随机
在实际开发的过程中,每个项目的端口其实都是在开发过程中被规定好的,通过server.port 进行配置,如果想启动多实例的时候就只需要修改端口号即可。但是在有些场景下,要根据线上的流量对实例进行动态的扩容,那么这个时候就需要能动态的去改变端口号,这个因为是全自动的,所以不可能人为的去修改端口然后启动实例。这个时候就可以通过如下的方式来实现端口号的随机
server.port=${random.int[2000,8000]}
通过上面一种方式来实现随机生成一个2000-8000的端口号,这样每次启动之后的端口号就不一样了,但是为了保证正常业务是可以访问的在整个的架构实现的过程中需要保证服务高可用。但是这样一来还有一个新问题出现,那就是如果该端口已经被使用了如果再次启动就会报错了。那么这个时候就需要进行如下的一个操作
public class StartCommand{
public StartCommand(String[] args){
Boolean isServerPort = false;
String serverPort = "";
if(args != null){
for(String arg;args){
if(StringUtils.hasText(arg)&&arg.startWith("--server.port")){
isServerPort = true;
serverPort = arg;
break;
}
}
}
if(!isServerPort){
int port = ServerPortUtils.getAvailablePort();
System.setProperty("server.port",String.valueOf(port))
}else{
System.setProperty("server.port",serverPort.split("=")[1])
}
}
}
通过对启动参数的遍历,如果指定启动端口,后续不需要自动生成,如果没有进行指定,就通过ServerPortUitls 进行指定,然后设置到环境变量中,在application.properties中通过如下的方式进行获取
server.port = ${server.port}
可以在启动类中加入如下的操作
new SrartCommand(args);
总结
在这次分享中主要是说了一些自定义的配置异常处理,异步执行操作以及端口随机等操作。对于日常的开发中还是比较重要的,学会了这些配置,并且理解了其原理,对提升开发效率会有很大的帮助。