1. 背景
由于微服务的流行,我们会动不动就建立一个新的项目作为一个服务,那么项目中的全局异常处理和统一数据格式是很重要的,如果设计不好,不仅开发时很乱,在查询日志时也会相当麻烦,所以我自己设计了一个简单的项目框架,个人感觉在小项目上会很好用,如果项目太大,可能需要再追求细节。
2. 项目的功能
2.1 整合了mybatis plus
2.2 整合了druid
2.3 整合了log4j2,并通过lombok,能更方便的打印日志
2.4 实现了统一数据返回格式(默认全部请求都是统一的返回格式)
2.5 实现了全局异常统一处理
2.6 利用aop打印了接口访问信息,如ip,接口访问时长等
3.功能详细讲解(由于某些文件内容过大,所以我就不在这里粘贴了,会在文章末尾提供一个项目下载地址的)
3.1 整合mybatis plus
由于本人喜欢用mybatis,所以就整合了mybatis plus作为简化mybatis操作的框架
3.1.1 安装mybatis plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
建议安装3.1.0,因为大于该版本会有一个bug,那就是不能正确映射LocalDateTime类,会报错,当然如果你喜欢使用Date类,那就完全没问题,LocalDateTime是java8推出的时间类,从api的使用便捷性和性能上看比Date都要好一点,该bug可参考:https://mp.baomidou.com/guide/faq.html#error-attempting-to-get-column-create-time-from-result-set-cause-java-sql-sqlfeaturenotsupportedexception
3.1.2 在启动类上添加@MapperScan注解,并指定mapper文件夹位置
3.1.3 mybatis plus的自动填充功能
由于每张表基本上都会有created_time,updated_time等公共属性,并且我们又想在创建时自动设置时间,而不用我们在service层中手动设置时间属性,这就可以使用mybatis的自动填充功能呢。
首先创建一个MyBatisPlusConfig配置类,开启他的自动填充功能
@Configuration
public class MyBatisPlusConfig {
/**
* 自动填充功能
* @return
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MetaHandler());
return globalConfig;
}
}
其次创建MetaHandler类,重写MetaObjectHandler接口中的方法即可,我的项目中有该文件,此处就不作展示了
3.2 整合druid
3.2.1 安装druid
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
3.2.2 创建一个application-druid.yml文件,并添加配置
3.2.3 在application.yml添加application-druid.yml配置,使其生效
spring:
profiles:
active:
- druid
- dev
3.2.4 创建DruidConfig配置类
至此,druid的配置就结束了,可通过localhost:8080/druid 查看sql访问情况
3.3 整合log4j2
3.3.1 引入依赖
<dependency> <!-- 引入log4j2依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
3.3.2 去除springboot默认使用的logback
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
3.3.3 创建log4j2-sit.xml文件,并放在src/main/resource的目录下
3.3.4 在application.yml中进行配置,使得springboot使用log4j2作为默认日志框架
logging:
config: classpath:log4j2-sit.xml
3.3.5 整合lombok,使用log打印日志
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
@Log4j2
public class TestService{
public void insertUser() {
log.info("aa");
}
}
以往我们打印日志都需要先 Logger log = LoggerFactory.getLogger(xxx)一下,然后再使用,这种使用方式太麻烦,第一是各种框架获取logger对象的方法是不一样的,导致你使用前还要去看看以前代码,当然如果你记忆力好,那也没啥问题,第二是要传入当前类的class。而集成了lombok后
通过两步就可以很方便打印日志了,第一步,在class上添加@log4j2注解,这是lombok的注解,如果没有该注解,请检查下使用添加了lombok的依赖,第二部,直接log对象打印即可。
3.3.6 当你使用了lombok的@Data注解,会遇到一个问题,当你继承了一个父类时,会有警告,此时需要在/src/main/java目录下创建一个lombok.config文件:
config.stopBubbling=true
lombok.equalsAndHashCode.callSuper=call
3.4 统一数据返回格式
3.4.1 思路
我设计的思路:由于目前是前后端分离,所以现在的后端项目基本上返回的都是json,并不会进行页面跳转控制。我们规定所有返回给前端的数据格式均为{code,msg,data}这三个参数,报错另说。我添加了一个拦截器,该拦截器会拦截所有请求,并判断controller层的方法上是否有被ResponseNature注解修饰,该注解是自定义的注解,如果没有,那么就添加一个attr在request中做一个标记,如果有该注解就不添加该attr。并添加一个controllerAdvice增强器,在被@responseBody处理数据前,对数据进行自定义的处理,处理时判断是否有在拦截器中做的attr标记,如果有,那么就说明需要做同一格式,没有,就不做。所以写代码时,不用添加任何注解,默认就是将数据进行统一返回格式处理,如果你不想处理,就要返回原本的对象,那么就在方法或者类上添加@ResponseNature注解
3.4.2 创建ResponseResultInterceptor类
/**
*
*/
package com.rewa.test.interceptor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.rewa.test.annotations.ResponseNature;
import com.rewa.test.util.DBConstants;
import com.rewa.test.util.RequestContextUtil;
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
/**
* 拦截请求,默认给所有请求添加RESPONSE_RESULT标识,如果controller层的类或方法有被ResponseNature注解修饰,那么就不添加标识,即不进行统一数据返回
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestContextUtil.setRequestId();
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
if (clazz.isAnnotationPresent(ResponseNature.class) || method.isAnnotationPresent(ResponseNature.class)) {
return true;
}
request.setAttribute(DBConstants.RESPONSE_RESULT,DBConstants.RESPONSE_RESULT);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
3.4.3 创建InterceptorConfig配置文件
package com.rewa.test.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.rewa.test.interceptor.ResponseResultInterceptor;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
public static String ALLPATH = "/**";
@Autowired
private ResponseResultInterceptor responseResultInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有的请求都要拦截
registry.addInterceptor(responseResultInterceptor).addPathPatterns(ALLPATH);
}
}
注意:有些人看别人的博客在开启拦截器时会添加@EnableWebMvc注解,该注解添加后会有非常多的问题,我个人不建议使用该注解,不添加该注解,拦截器也是可以正常工作的,具体有哪些坑,我遇到的一个是application.yml里面对json的配置失效了,即
spring:
jackson:
default-property-inclusion: non-null
具体有哪些坑,可以参考该文章:https://blog.csdn.net/zxc123e/article/details/84636521
通过以上配置,我们已经成功对request进行了标识,现在我们就要开始处理返回数据了
3.4.4 添加ResponseResultHandle类
package com.rewa.test.handle;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.rewa.test.constants.DBConstants;
import com.rewa.test.msg.response.UnitiveResponse;
import com.rewa.test.util.JsonUtil;
import com.rewa.test.util.RequestContextUtil;
@ControllerAdvice
public class ResponseResultHandle implements ResponseBodyAdvice<Object>{
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
HttpServletRequest request = RequestContextUtil.getRequest();
String flag = (String) request.getAttribute(DBConstants.RESPONSE_RESULT);
return flag != null && flag.equals(DBConstants.RESPONSE_RESULT);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof UnitiveResponse) {
return body;
} else if (body instanceof String) {
return JsonUtil.object2Json(UnitiveResponse.success(body));
}else {
return UnitiveResponse.success(body);
}
}
}
support方法是用来判断是否需要执行下面的beforeBodyWrite方法,通过代码可以发现,只有request中有了RESPONSE_RESULT标识才允许执行beforeBodyWrite方法,那在beforeBodyWrite中只需要使用创建出一个统一格式返回的对象即可:UnitiveResponse类如下所示,我们可以暂时只看success方法,BusinessException 是自定义的异常对象,可以先不考虑。注意,当返回值为String的时候,处理是有区别的,所以可以看到我对String进行了单独处理
/**
*
*/
package com.rewa.test.msg.response;
import com.rewa.test.enums.ResultCode;
import com.rewa.test.exception.BusinessException;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author thinker
*
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UnitiveResponse{
private Integer code;
private String msg;
private Object data;
// 错误原因
protected String mainCause;
// 详细堆栈信息
protected String trace;
// 请求id,唯一的
private Long requestId;
public static UnitiveResponse success() {
UnitiveResponse result = new UnitiveResponse();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
public static UnitiveResponse success(Object data) {
UnitiveResponse result = new UnitiveResponse();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
private void setResultCode(ResultCode code) {
this.code = code.code();
this.msg = code.message();
}
public static UnitiveResponse error(BusinessException e) {
UnitiveResponse u = new UnitiveResponse();
u.setCode(e.getCode());
u.setMainCause(e.getMainCause());
u.setMsg(e.getMessage());
u.setTrace(e.getTrace());
u.setRequestId(e.getRequestId());
return u;
}
}
ResultCode是一个枚举,其中包含了所有的提示,包括正确的和错误的
/**
*
*/
package com.rewa.test.enums;
/**
* @author thinker
*
*/
public enum ResultCode {
/* 成功状态码 */
SUCCESS(0, "成功"),
/* 系统错误码 */
SYSTEM_INNER_ERROR(-1, "The system is busy, please try again"),
//前端错误
//后端错误
TEST(2,"test fail {}");
private Integer code;
private String message;
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public static String getMessage(String name) {
for (ResultCode item : ResultCode.values()) {
if (item.name().equals(name)) {
return item.message;
}
}
return name;
}
public static Integer getCode(String name) {
for (ResultCode item : ResultCode.values()) {
if (item.name().equals(name)) {
return item.code;
}
}
return null;
}
@Override
public String toString() {
return this.name();
}
}
至此统一数据格式返回就已经完成了
3.5 全局异常处理
3.5.1 创建一个BusinessException类,代表所有的业务异常,我们这里没分那么细,该异常就代表了所有异常
package com.rewa.test.exception;
import java.util.Arrays;
import com.rewa.test.enums.ResultCode;
import com.rewa.test.util.RequestContextUtil;
import com.rewa.test.util.StrUtils;
import lombok.Data;
@Data
public class BusinessException extends RuntimeException{
/**
*
*/
private static final long serialVersionUID = 3275057317616326272L;
protected Integer code;
protected String message;
// 原因
protected String mainCause;
// 详细堆栈信息,只取了前5条
protected String trace;
private Long requestId;
public BusinessException (Integer code,String message, Exception e) {
this.requestId = RequestContextUtil.getRequestId();
this.code = code;
this.message = message;
this.mainCause = e.getMessage();
this.trace = getStackMsg(e);
}
public BusinessException (String message) {
this.requestId = RequestContextUtil.getRequestId();
this.code = ResultCode.SYSTEM_INNER_ERROR.code();
this.message = message;
}
public BusinessException(Integer code, String format,Exception e, Object... objects) {
this.requestId = RequestContextUtil.getRequestId();
this.code = code;
this.message = StrUtils.formatIfArgs(format, "{}", objects);
this.mainCause = e.getMessage();
this.trace = getStackMsg(e);
}
public BusinessException(ResultCode resultCode) {
this.requestId = RequestContextUtil.getRequestId();
this.code = resultCode.code();
this.message = resultCode.message();
}
public BusinessException(ResultCode resultCode, Object... objects) {
this.requestId = RequestContextUtil.getRequestId();
this.code = resultCode.code();
this.message = StrUtils.formatIfArgs(resultCode.message(), "{}", objects);
}
public BusinessException(ResultCode resultCode,Exception e, Object... objects) {
this.requestId = RequestContextUtil.getRequestId();
this.code = resultCode.code();
this.message = StrUtils.formatIfArgs(resultCode.message(), "{}", objects);
this.mainCause = e.getMessage();
this.trace = getStackMsg(e);
}
public BusinessException(ResultCode resultCode,Exception e) {
this(resultCode);
this.mainCause = e.getMessage();
this.trace = getStackMsg(e);
}
private static String getStackMsg(Exception e) {
StackTraceElement[] copyOfRange = Arrays.copyOfRange(e.getStackTrace(), 0, 5);
return Arrays.toString(copyOfRange);
}
}
3.5.2 创建GlobalExceptionHandle类用来拦截所有的异常
package com.rewa.test.handle;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.rewa.test.enums.ResultCode;
import com.rewa.test.exception.BusinessException;
import com.rewa.test.msg.response.UnitiveResponse;
import lombok.extern.log4j.Log4j2;
@RestControllerAdvice
@Log4j2
public class GlobalExceptionHandle {
@ExceptionHandler(value = BusinessException.class)
public UnitiveResponse errorHandler(BusinessException e) {
log.error("请求id:"+e.getRequestId() + ",错误原因:" + e.getMainCause(),e);
return UnitiveResponse.error(e);
}
@ExceptionHandler(value = Exception.class)
public UnitiveResponse errorHandler(Exception e) {
BusinessException ex = new BusinessException(ResultCode.SYSTEM_INNER_ERROR,e);
log.error("错误id:"+ex.getRequestId() + ",错误原因:" + ex.getMainCause(),ex);
return UnitiveResponse.error(ex);
}
}
3.5.3 使用时有两种抛异常的方式,第一种手动try-catch后抛出,第二是没有捕获到直接抛出
对于第一种,类似下面这种形式
public void getUser() {
try {
System.out.println(1/1);
}catch (Exception e) {
throw new BusinessException("sssssss");
}
}
当手动抛出时,会匹配到GlobalExceptionHandle类中的第一个方法,然后返回一个UnitiveResponse对象
对于第二种,即没有try-catch就直接抛出异常的情况,会匹配到GlobalExceptionHandle类中的第二个方法,返回一个UnitiveResponse对象
3.6 利用aop打印接口访问信息
3.6.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.6.2 开启aop功能
在启动类上添加@EnableAspectJAutoProxy注解
3.6.3 创建ApiLogAop文件
package com.rewa.test.aop;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import com.rewa.test.util.RequestContextUtil;
import lombok.extern.log4j.Log4j2;
@Aspect
@Configuration
@Log4j2
public class ApiLogAop {
public static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 定义一个切入点.
* 解释下:
*
*/
@Pointcut("execution(* com.rewa.test.controller.*.*(..))")
public void webLog(){}
@Around("webLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object result = null;
long startTime = System.currentTimeMillis();
try {
result = joinPoint.proceed();
} catch (Throwable e) {
logInfo(joinPoint,startTime,false);
throw e;
}
logInfo(joinPoint,startTime,true);
return result;
}
public void logInfo (ProceedingJoinPoint joinPoint,long startTime,boolean status) {
long endTime = System.currentTimeMillis();
HttpServletRequest request = RequestContextUtil.getRequest();
String requestMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
StringBuffer sb = new StringBuffer("");
sb.append("api接口访问日志-----").
append("请求id:").append(RequestContextUtil.getRequestId()).append(",").
append("执行方法:").append(requestMethod).append(",").
append("执行状态:").append(status == true?"成功":"失败").append(",").
append("执行时间:").append(dtf.format(LocalDateTime.now())).append(",").
append("耗时(毫秒):").append(endTime-startTime).append(",").
append("URL:").append(request.getRequestURL().toString()).append(" ").append(request.getMethod()).append(",").
append("IP:").append(request.getRemoteAddr()).append(",").
append("ARGS:").append(Arrays.toString(joinPoint.getArgs()));
log.info(sb.toString());
}
}
4. 注意点
4.1 其实做全局异常处理有两种方法,第一种就是通过@ControllerAdvice,第二种就是通过aop来做,但是要注意这两种方法的调用先后顺序
aop中如下处理异常,即通过try-catch来做
@Around("webLog()")
/**
*要有返回值,否则就返回不了joinPoint.proceed()的返回值了
*/
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object result = null;
long startTime = System.currentTimeMillis();
try {
result = joinPoint.proceed();
} catch (Throwable e) {
logInfo(joinPoint,startTime,false);
throw e;
}
logInfo(joinPoint,startTime,true);
return result;
}
所以执行的顺序应该是先被aop中的异常捕获,如果在catch时throw了这个异常,这时才会被@ControllerAdvice的异常机制处理
4.2 我在返回错误时,我返回了一个requestId,该requestId就是在拦截器中设置到request的attr中的,这个requestId还是很有用的,可以在log中快速定位到错误
4.3 分布式唯一id问题(Long转String)
现在分布式唯一id一般都是使用long类型,但是前端js在处理long类型时会出现精度问题,所以在返回给前端时,应该bean中的id全部转为string类型,但是bean的属性已经被定义为Long了,应该怎么改呢?可以使用@JsonSerialize(using=Long2StringHandle.class)注解,该注解是springboot自带的,Long2StringHandle是一个自定义的class,专门是用来处理Long转String
public class Long2StringHandle extends JsonSerializer<Long>{
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(String.valueOf(value));
}
}
4.4 当controller层方法上有response参数引起的问题
一共有2种情况,如代码所示
// 1.情况1,返回值为void
@RequestMapping(value="/log",method=RequestMethod.GET)
public void log(HttpServletResponse response) {
}
//2. 情况2,返回值为对象
@RequestMapping(value="/log",method=RequestMethod.GET)
public User log(HttpServletResponse response) {
return new User("tfp");
}
情况1,正常情况下应该是{code: 0,msg:"成功"},但是实际上会导致没有任何结果返回
情况2,和正常情况一致,没有任何问题
所以这就说明了当controller层方法上有response参数时,会导致结果和想象中的不一致,这是因为springboot源码对于这种情况有特殊处理的,所以建议不要在controller层上写response参数,完全可以使用注入对象的方式。如果你非要使用response参数,并且返回值还是void,那么你至少需要抛出一个异常才行,否则返回值为空
@Autowired
protected HttpServletResponse response;
4.5 当返回值为String时的特殊处理
@RequestMapping(value="/log",method=RequestMethod.GET)
public String log(HttpServletResponse response) {
return "11";
}
当返回值为String时,在response时是需要特殊处理的,否则会报错:UnitiveResponse类型转不了String类型
package com.rewa.test.handle;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.rewa.test.constants.DBConstants;
import com.rewa.test.msg.response.UnitiveResponse;
import com.rewa.test.util.JsonUtil;
import com.rewa.test.util.RequestContextUtil;
@ControllerAdvice
public class ResponseResultHandle implements ResponseBodyAdvice<Object>{
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
HttpServletRequest request = RequestContextUtil.getRequest();
String flag = (String) request.getAttribute(DBConstants.RESPONSE_RESULT);
return flag != null && flag.equals(DBConstants.RESPONSE_RESULT);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof UnitiveResponse) {
return body;
} else if (body instanceof String) {
return JsonUtil.object2Json(UnitiveResponse.success(body));
}else {
return UnitiveResponse.success(body);
}
}
}
5 说明
可能你看这篇文章会有点难看懂,但是如果你结合整个项目来看,就会很容易看懂了。