基于springboot整合了mybatis plus,lombok,log4j2并实现了全局异常处理及统一数据返回格式(code,msg,data)

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


    com.baomidou
    mybatis-plus-boot-starter
    3.1.0

 建议安装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


    com.alibaba
    druid
    1.1.16

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 引入依赖

   
	 org.springframework.boot  
	 spring-boot-starter-log4j2  
 

3.3.2 去除springboot默认使用的logback


	org.springframework.boot
	    spring-boot-starter-web
		  
		     
		      org.springframework.boot  
		      spring-boot-starter-logging  
		  
      

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打印日志


	org.projectlombok
	lombok
	true
@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{

	@Override
	public boolean supports(MethodParameter returnType, Class> 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> 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 添加依赖


	org.springframework.boot
	spring-boot-starter-aop

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{
	@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{

	@Override
	public boolean supports(MethodParameter returnType, Class> 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> 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 说明

可能你看这篇文章会有点难看懂,但是如果你结合整个项目来看,就会很容易看懂了。

项目地址:https://github.com/tanfangping/springboot-base-template

你可能感兴趣的:(基于springboot整合了mybatis plus,lombok,log4j2并实现了全局异常处理及统一数据返回格式(code,msg,data))