【随笔】Springboot实现全局异常处理starter

前言

我们在日常开发中经常会和异常打交道,相信谁也不会陌生。为了提高工作效率和快速定位问题,本人将在项目中异常处理的经验总结并将设计为一个异常starter用于学习交流,有共同爱学习的伙伴可以看过来。

一、项目工程结构定义

包名说明
core抽象核心模块,包含api、context子包
exception异常模块,包含configuration、utils、view、web.controller、web.filter、web.handler子包
在这里插入图片描述在这里插入图片描述

二、SpringContext工具类实现

类名说明
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));
    }
}

三、API返回结果模型设计

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;
    }
}

四、常规异常模型定义设计

  • 模型组件

在这里插入图片描述

  • ApplicationException定义
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);
    }
}
  • Exception数据Template
## 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}
  • TraceLogFilter实现
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);

    }
}
  • ExceptionResult模型
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;
    }
}
  • ExceptionUtils实现
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 <T> 返回类型
     * @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");
    }
}
  • ApplicationExceptionHandler实现
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 );
    }
}

五、Filter中的异常处理

  • 模型组件优化(新增处理异常的filter和处理filter异常的控制器)

在这里插入图片描述

  • TryExceptionFilter实现
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);

}
  • ExceptionController实现
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;
        }
    }
}

七、异常类自动装配类实现

  • 本项目作为Starter组件无启动类使用ExceptionAutoConfiguration做配置类
  • 使用@ComponentScan(“com.imk.cases.springboot.exception”)扫描bean
  • 使用@PropertySource加载异常定义信息
  • 使用自定义注解@EnableException完成自动装配
  • 具体代码实现
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支持异常处理方案

若新增的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);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值