我们在日常开发中经常会和异常打交道,相信谁也不会陌生。为了提高工作效率和快速定位问题,本人将在项目中异常处理的经验总结并将设计为一个异常starter用于学习交流,有共同爱学习的伙伴可以看过来。
包名 | 说明 |
---|---|
core | 抽象核心模块,包含api、context子包 |
exception | 异常模块,包含configuration、utils、view、web.controller、web.filter、web.handler子包 |
类名 | 说明 |
---|---|
AppContextInitializer | 上下文初始化器 |
AppContextUtils | 上下文工具 |
package com.imk.cases.springboot.core.context;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* 初始化Spring文到上下文工具类中
*
* @author darrn.xiang
* @date 2022/8/25 20:45
*/
public class AppContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
AppContextUtils.setApplicationContext(applicationContext);
}
}
package com.imk.cases.springboot.core.context;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
/**
* 记录Spring容器上下文
*
* @author darrn.xiang
* @date 2022/8/25 17:11
*/
public class AppContextUtils {
private static ApplicationContext applicationContext = null;
private static Environment environment = null;
public synchronized static void setApplicationContext(ApplicationContext applicationContext){
if (AppContextUtils.applicationContext == null) {
AppContextUtils.applicationContext = applicationContext;
environment = applicationContext.getEnvironment();
}
}
public static ApplicationContext getContext() {
return applicationContext;
}
public static Environment getEnvironment() {
return environment;
}
public static Object getBean(String name) {
return getContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getContext().getBean(name, clazz);
}
public static String getString(String key) {
return environment.getProperty(key);
}
public static String getString(String key,String defaultValue) {
return environment.getProperty(key,defaultValue);
}
public static int getInt(String key) {
return Integer.valueOf(environment.getProperty(key));
}
public static long getLong(String key) {
return Long.valueOf(environment.getProperty(key));
}
public static boolean getBoolean(String key) {
return Boolean.valueOf(environment.getProperty(key));
}
}
package com.imk.cases.springboot.core.api;
import com.imk.cases.springboot.exception.view.ExceptionResult;
import lombok.Data;
/**
* API返回视图
*
* @author darrn.xiang
* @date 2022/8/14 16:58
*/
@Data
public class ApiResult {
enum Status{
Success,
Failure
}
private String status;
private String message;
private Object data;
public static ApiResult success(Object data){
ApiResult result = new ApiResult();
result.setStatus(Status.Success.toString());
result.setData(data);
return result;
}
public static ApiResult success(){
ApiResult result = new ApiResult();
result.setStatus(Status.Success.toString());
return result;
}
public static ApiResult fail(String message){
ApiResult result = new ApiResult();
result.setStatus(Status.Failure.toString());
result.setMessage(message);
return result;
}
public static ApiResult fail(ExceptionResult apiExceptionResult){
ApiResult result = new ApiResult();
result.setStatus(Status.Failure.toString());
result.setMessage(apiExceptionResult.getErrorMessage());
result.setData(apiExceptionResult);
return result;
}
}
package com.imk.cases.springboot.exception;
import com.imk.cases.springboot.exception.utils.ExceptionUtils;
import lombok.Getter;
import org.springframework.util.StringUtils;
/**
* 应用异常定义
*
* @author darrn.xiang
* @date 2022/8/25 17:11
*/
@Getter
public class ApplicationException extends RuntimeException {
private String errorCode;
private Object[] args;
public ApplicationException(){
super();
}
public ApplicationException(String errorCode){
super(errorCode);
this.errorCode = errorCode;
}
public ApplicationException(String errorCode, Object... args){
super(errorCode);
this.errorCode = errorCode;
this.args = args;
}
public ApplicationException(Throwable throwable){
super(throwable);
this.errorCode = throwable.getMessage();
}
public ApplicationException(String errorCode, Throwable throwable){
super(throwable);
this.errorCode = errorCode;
}
public String getErrorMessage(){
return getErrorMessage(ExceptionUtils.getLang());
}
public String getErrorMessage(String lang){
if(!StringUtils.hasLength(errorCode)){
return getMessage();
}
return ExceptionUtils.getMessage(errorCode, args, lang);
}
}
## APP_100001=APP_100001|304|zhCN=中语;enUS=英语 PS:国际化内容中不能出现;
SYS_100001=SYS_100001|500|zhCN=发生未知异常,异常信息为:{0};enUS=An unknown exception occurred, the exception information is:{1}
package com.imk.cases.springboot.exception.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
import java.util.UUID;
/**
* 日志跟踪处理(通过线程名称记录traceId)
* * @author darrn.xiang
* @date 2022/8/20 19:35
*/
@Slf4j
public class TraceLogFilter extends TryExceptionFilter {
@Override
public void doService(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
try {
initTraceId(servletRequest);
}catch (RuntimeException exception){
log.error("TraceLogFilter initTraceId error. exception={}",exception);
}
}
private void initTraceId(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
// traceId模板 主机名称|线程名称|traceId
String template = "WebContainer:{0}|[Thread]{1}|[TraceId]{2}";
String traceIdStr = MessageFormat.format(template, request.getLocalAddr(), Thread.currentThread().getName(), UUID.randomUUID().toString());
Thread.currentThread().setName(traceIdStr);
}
}
package com.imk.cases.springboot.exception.view;
import com.imk.cases.springboot.exception.ApplicationException;
import com.imk.cases.springboot.exception.utils.ExceptionUtils;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.util.Map;
@Data
public class ExceptionResult {
public String errorCode;
public String errorMessage;
public int httpCode;
public String traceId;
public static ExceptionResult of(ApplicationException exception){
String lang = ExceptionUtils.getLang();
Map<String, String> messageMap = ExceptionUtils.getMessageMap(exception, lang);
return of(messageMap);
}
public static ExceptionResult of(String errorCode, String exMsg){
String lang = ExceptionUtils.getLang();
// 解决解析失败问题,替换异常中的";"
if(StringUtils.hasLength(exMsg)){
exMsg = exMsg.replaceAll(";",",");
}else{
exMsg = "Null pointer exception.";
}
String message = ExceptionUtils.getMessage(errorCode, new Object[]{exMsg,exMsg});
Map<String, String> messageMap = ExceptionUtils.getMessageMap(message, lang);
return of(messageMap);
}
public static ExceptionResult of(Map<String, String> messageMap){
ExceptionResult apiExceptionResult = new ExceptionResult();
apiExceptionResult.setErrorCode(messageMap.get("errorCode"));
apiExceptionResult.setErrorMessage(messageMap.get("errorMessage"));
apiExceptionResult.setHttpCode(Integer.parseInt( messageMap.get("httpCode") ));
apiExceptionResult.setTraceId(Thread.currentThread().getName());
return apiExceptionResult;
}
}
package com.imk.cases.springboot.exception.utils;
import com.imk.cases.springboot.core.context.AppContextUtils;
import com.imk.cases.springboot.exception.ApplicationException;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
/**
* 异常工具包
* * @author darrn.xiang
* @date 2022/8/25 17:11
*/
public class ExceptionUtils {
/**
* 错误信息分割符
*/
public static final String MSG_DELIMITER_STR = "\\|";
/**
* 国际化分割符
*/
public static final String I18N_DELIMITER_STR = ";";
/**
* 字段对应的位置
*/
public static final int FILED_ERROR_CODE = 0;
public static final int FILED_HTTP_CODE =1;
public static final int FILED_I18N = 2;
public static final String RETHROW_EXCEPTION = "RETHROW_EXCEPTION";
public static final String RETHROW_EXCEPTION_API = "/exceptions/rethrow";
/**
* 返回配置文件配置的异常信息,默认格式
*
* @param errorCode 错误编码
* @return 异常信息
*/
public static String getMessage(String errorCode){
Environment environment = AppContextUtils.getEnvironment();
String message = environment.getProperty(errorCode);
// 解决中文乱码问题
if(StringUtils.hasLength(message)){
message = new String(message.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
}
return message;
}
/**
* 返回错误信息,格式占位符的信息
*
* @param errorCode 错误编码
* @param args 占位符参数
* @return 异常信息
*/
public static String getMessage(String errorCode,Object[] args){
// 如果错误编码没有配置,直接返回错误编码
String message = getMessage(errorCode);
if(!StringUtils.hasLength(message)){
message = errorCode;
}
// 参数占位符替换
if(args != null && args.length > 0){
message = MessageFormat.format(message,args);
}
return message;
}
/**
* 获取指定语种的错误信息
*
* @param errorCode 错误编码
* @param args 占位参数
* @param lang 语种
* @return 指定语种的错误信息,格式:100001_500_服务访问异常
*/
public static String getMessage(String errorCode,Object[] args,String lang){
String message = getMessage(errorCode, args);
if(!StringUtils.hasLength(lang)){
return message;
}
// 格式为:zh=中语,en=英语,ph=菲律宾语
String i18n = getMessageFieldValue(message,FILED_I18N);
Map<String, String> langMap = getLangMap(i18n);
String tipMessage = langMap.get(lang);
return message.replace(i18n,tipMessage);
}
public static String getMessage(String errorCode,String lang){
return getMessage(errorCode,null,lang);
}
/**
* 把国际化消息转为map格式
*
* @param i18nMessage 国际化消息
* @return
*/
public static Map<String,String> getLangMap(String i18nMessage){
String[] langs = i18nMessage.split(I18N_DELIMITER_STR);
Map<String,String> langMap = new HashMap<>();
for (String str:langs ) {
String[] keyAndValue=str.split("=");
langMap.put(keyAndValue[0].trim(),keyAndValue[1].trim());
}
return langMap;
}
/**
* 是否为配置文件的异常分隔格式
*
* @param message 异常编码对应的异常消息
* @return true/false
*/
public static boolean isPropertiesFormat(String message){
if(message.indexOf("|")<0){
return false;
}
return true;
}
/**
*获取错误消息中具体的字段值
*
* @param message 格式为:error_001=errorCode_httpCode_i18nMessage(zhCN=中语,enUS=英语)
* errorCode=0,httpCode=1,i18n=2
* @param 返回类型
* @return 字段对应的值
*/
public static <T> T getMessageFieldValue(String message,int fieldIndex){
if (!isPropertiesFormat(message)){
return (T)message;
}
return (T)message.split(MSG_DELIMITER_STR)[fieldIndex];
}
/**
* 将错误信息转为map
* @param exception 异常信息
* @return mp格式的消息
*/
public static Map<String,String> getMessageMap(ApplicationException exception, String lang){
String errorMessage = exception.getErrorMessage();
return getMessageMap( errorMessage, lang);
}
/**
* 将错误信息转为map
* @param errorMessage 异常信息
* @return mp格式的消息
*/
public static Map<String,String> getMessageMap(String errorMessage, String lang){
Map<String,String> map = new HashMap<>();
map.put("errorCode",getMessageFieldValue(errorMessage,FILED_ERROR_CODE));
map.put("httpCode",getMessageFieldValue(errorMessage,FILED_HTTP_CODE));
String i18n = getMessageFieldValue(errorMessage, FILED_I18N);
if(i18n.contains(lang)){
map.put("errorMessage",getLangMap(i18n).get(lang));
}else{
map.put("errorMessage",i18n);
}
return map;
}
public static String getLang(){
return AppContextUtils.getString("app.i18n.lang","zhCN");
}
}
package com.imk.cases.springboot.exception.web.handler;
import com.imk.cases.springboot.exception.ApplicationException;
import com.imk.cases.springboot.exception.view.ExceptionResult;
import com.imk.cases.springboot.core.api.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class ApplicationExceptionHandler {
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<Object> handle(final ApplicationException exception){
log.error("ApplicationExceptionHandler handle exception={}",exception.getErrorMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ExceptionResult data = ExceptionResult.of(exception);
return new ResponseEntity<>(ApiResult.fail(data), headers, HttpStatus.resolve(data.getHttpCode()) );
}
}
package com.imk.cases.springboot.exception.web.handler;
import com.imk.cases.springboot.exception.view.ExceptionResult;
import com.imk.cases.springboot.core.api.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*
* @author darrn.xiang
* @date 2022/8/14 17:28
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handle(final Exception exception){
// 异常为空打印堆栈信息
if(StringUtils.hasLength(exception.getMessage())){
log.error("GlobalExceptionHandler exception={}",exception.getMessage());
}else{
log.error("GlobalExceptionHandler exception.",exception);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
try{
ExceptionResult data = ExceptionResult.of("SYS_100001",exception.getMessage());
return new ResponseEntity<>(ApiResult.fail(data), headers, HttpStatus.resolve(data.getHttpCode()) );
}catch (Exception exception1){
log.error("cast ApiExceptionResult error. exception1={}",exception1.getMessage());
}
return new ResponseEntity<>(ApiResult.fail(exception.getMessage()), headers, HttpStatus.INTERNAL_SERVER_ERROR );
}
}
package com.imk.cases.springboot.exception.web.filter;
import com.imk.cases.springboot.exception.utils.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import java.io.IOException;
/**
* 异常处理过滤器
*
* @author darrn.xiang
* @date 2022/8/20 20:18
*/
@Slf4j
public abstract class TryExceptionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
// 业务实现
doService(servletRequest,servletResponse,filterChain);
filterChain.doFilter(servletRequest,servletResponse);
}catch (RuntimeException exception){
log.error("TestExceptionFilter doFilter error. ex=",exception.getMessage());
// 异常转发到控制器处理
servletRequest.setAttribute(ExceptionUtils.RETHROW_EXCEPTION,exception);
servletRequest.getRequestDispatcher(ExceptionUtils.RETHROW_EXCEPTION_API).forward(servletRequest,servletResponse);
}
}
/**
* 自定义业务实现
*
* @param servletRequest 请求信息
* @param servletResponse 响应信息
* @param filterChain 链路
*/
public abstract void doService(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain);
}
package com.imk.cases.springboot.exception.web.controller;
import com.imk.cases.springboot.exception.ApplicationException;
import com.imk.cases.springboot.exception.utils.ExceptionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 处理转发的异常信息API
*
* @author darrn.xiang
* @date 2022/8/20 20:22
*/
@RestController
public class ExceptionController {
@RequestMapping(ExceptionUtils.RETHROW_EXCEPTION_API)
public void throwException(HttpServletRequest request){
Object filterException = request.getAttribute(ExceptionUtils.RETHROW_EXCEPTION);
if(filterException instanceof ApplicationException){
throw (ApplicationException)filterException;
}else{
throw (RuntimeException)filterException;
}
}
}
package com.imk.cases.springboot.exception.configuration;
import com.imk.cases.springboot.exception.web.filter.CharacterEncodeFilter;
import com.imk.cases.springboot.exception.web.filter.TraceLogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.ArrayList;
import java.util.List;
/**
* 异常处理自动装配
*
* @author darrn.xiang
* @date 2022/8/25 21:09
*/
@Configuration
@ComponentScan("com.imk.cases.springboot.exception")
@PropertySource(name = "ExceptionConfig",value = {
"classpath:defaultException.properties",
"classpath:exception.properties"
})
public class ExceptionAutoConfiguration {
/**
* 注册跟踪日志的过滤器
* @return 过滤器
*/
@Bean
public FilterRegistrationBean<TraceLogFilter> traceLogFilter(){
FilterRegistrationBean<TraceLogFilter> registrationBean = new FilterRegistrationBean<>();
TraceLogFilter myFilter = new TraceLogFilter();
registrationBean.setFilter(myFilter);
List<String> urls = new ArrayList<>();
//配置过滤规则
urls.add("/*");
registrationBean.setUrlPatterns(urls);
registrationBean.setOrder(1);
return registrationBean;
}
/**
* 字符编码过滤器
* @return 过滤器
*/
@Bean
public FilterRegistrationBean<CharacterEncodeFilter> characterEncodeFilter(){
FilterRegistrationBean<CharacterEncodeFilter> registrationBean = new FilterRegistrationBean<>();
CharacterEncodeFilter characterEncodeFilter = new CharacterEncodeFilter();
registrationBean.setFilter(characterEncodeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
registrationBean.setOrder(2);
return registrationBean;
}
}
package com.imk.cases.springboot.exception;
import com.imk.cases.springboot.exception.configuration.ExceptionAutoConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 开启异常处理类
*
* @author darrn.xiang
* @date 2022/8/25 22:12
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(ExceptionAutoConfiguration.class)
@Documented
public @interface EnableException {
}
目前自动装配是使用自动注解,如项目依赖使用,在导入jar后还需要在启动类上加入@EnableException才能生效;如需要导入jar自动注入,则需要在配置文件中spring.factories中加上org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.imk.cases.springboot.exception.configuration.ExceptionAutoConfiguration。
目前异常信息是支持国际化配置的,多种国际化通过“;”分隔。代码中固定zhCN即默认中文,如需更改可以在application.properties文件中配置,如:app.i18n.lang=zhCN
若新增的Filter需要支持异常处理方案,只需要继承TryExceptionFilter类实现doService即可;当然可以手动try-catch然后使用如下代码:
try {
// TODO 业务实现
filterChain.doFilter(servletRequest,servletResponse);
}catch (RuntimeException exception){
log.error("TestExceptionFilter doFilter error. ex=",exception.getMessage());
// 异常转发到控制器处理
servletRequest.setAttribute(ExceptionUtils.RETHROW_EXCEPTION,exception);
servletRequest.getRequestDispatcher(ExceptionUtils.RETHROW_EXCEPTION_API).forward(servletRequest,servletResponse);
}