Springboot 异常处理,实现运行时刷新返回提示语

以下所有代码均是伪代码, 并不保证运行,如果需要完整代码私聊我

异常处理由来

  1. 前端给用户显示提示信息
  2. 后端记录异常日志,查错,并保证程序正常运行

前端提示信息

前端代码

  1. 前端根据后端给的errorCode判断,然后前端给出提示信息
// 发起http请求
result = this.axios.get("xxxx");
// 如果成功直接返回结果
if (result.status == 200) {
	return result;
}

// 不成功根据errorCode 弹出提示信息
errorCode = result.errorCode;
if (errorCode == 100000) {
	alert("用户未登录");
} else if (errorCode == 100001) {
	alert("用户未认证");
} else if() {}
...........等等一系列判断

  1. 前端直接显示后端直接给的提示信息
// 发起http请求
result = this.axios.get("xxxx");
if (result.status != 200) {
	alert(result.msg);
}

根据上面前端的代码来看,前端肯定喜欢第二种方式。
而且如果我们要做国际化,前端要维护的东西就更多,更复杂了。

后端代码

设计要素:

  • 要记录日志,出的什么错,错误栈,相关的输入参数。
  • 返回提示信息,可以支持国际化
  • 错误信息存储在内存中提高效率,同时是否可以支持运行时更新提示信息
初步设计
//自定义异常类型枚举
public enum ExceptionTypeEnum {
    USER_NOT_EXIST(100002),
    USER_NOT_AUTH(100001),
    USER_NOT_LOGIN(100000);
    
    final int code;
    
    ExceptionTypeEnum(int code) { this.code = code; }
    public int getCode() { return code; }
    public String getName() { return name(); }
}

// 自定义异常类
public class ServiceException extends Exception {
	// 要记录在日志里面的相关输入参数
	private final String logDataString;

	// 前端提示信息
	private final String tipMessage;

	// 出错的类型,这里使用枚举类型,定义出所有的可能出错类型
	private final ExceptionTypeEnum exceptionTypeEnum;	
	
	// 省略掉getter , setter 和构造方法
	
	// 返回异常类型的名字
	public String getExceptionTypeEnumName() {
		return exceptionTypeEnum.name();
	}
}

抛出异常

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/exception/i18n")
    public void testExceptionI18n(@RequestParam String username) throws ServiceException {
        throw new ServiceException(
        	ExceptionTypeEnum.USER_NOT_LOGIN,
         	"logDataString username = " + username, 
         	"tipMessage 用户未登录"
         );
    }
}

SpringBoot 全局异常处理

@ControllerAdvice
@RestController
public class GlobalExceptionHandler {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(ServiceException.class)
    public final String serviceExceptionHandler(ServiceException serviceException, WebRequest request) {
    	//记录出错异常类型名
    	log.info(serviceException.getExceptionTypeEnumName());
    	// 记录出错时相关输入数据到日志里
        log.info(serviceException.getLogDataString());
        // 这里直接返回提示信息,理论上应该包装成一个统一返回格式的
        return serviceException.getTipMessage();
    }
}

初步设计是可以满足后端记录出错类型和相关参数的。
前端也可以直接将返回的提示信息输出。

缺点:

  • 代码中出现大量中文,污染代码
  • 不能支持国际化,如果要支持,得修改每一处抛出异常的地方,不利于扩展
  • 不能运行期间修改提示信息
进阶设计

考虑到springboot本身支持国际化,我们利用这个改造
在resource文件下新建i18n文件夹
这里需要支持几国语言就建立几个对应文件
在i18n下新建exception.properties文件

USER_NOT_EXIST = 用户不存在
USER_NOT_LOGIN = 用户未登录,请先登录

在i18n下新建exception_en_US.properties文件

USER_NOT_EXIST = User not exist
USER_NOT_LOGIN = User not login

在application.properties文件中添加如下配置

spring.messages.basename=i18n/exception
spring.messages.encoding=UTF-8

抛出异常

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/exception/i18n")
    public void testExceptionI18n(@RequestParam String username) throws ServiceException {
        throw new ServiceException(
        	ExceptionTypeEnum.USER_NOT_LOGIN,
         	"logDataString username = " + username
         );
    }
}

全局异常处理返回提示信息支持国际化

@ControllerAdvice
@RestController
public class GlobalExceptionHandler {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(ServiceException.class)
    public final String serviceExceptionHandler(ServiceException serviceException, WebRequest request) {
        // 获取当前国家
        Locale locale = LocaleContextHolder.getLocale();
        String message;
        try {
        	//通过messageSource获取对应异常类型名的提示信息
            message = messageSource.getMessage(serviceException.getName(), null, locale);
        } catch (Exception e) {
            e.printStackTrace();
            // 如果不存在, 就直接返回异常类型名
            message = serviceException.getName();
        }
        return message;
    }
}

国际化支持完毕,同时我们将提示语从代码中摘除了,放到了配置文件中,
方便统一管理,代码也干净了许多

运行期间修改提示信息怎么搞呢?
很自然就能想到配置中心

这里使用nacos存储配置文件。
下面是项目启动后从nacos下载配置文件,并存储到classpath下给国际化MessageSource使用。
同时添加监听器,监听nacos配置中心是否更改了内容,
如果内容修改了,就更新到本地

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.Executor;

@Slf4j
@Component
public class NacosConfig {

    private final String baseFolder = "i18n";

    private final String baseName = "exception";
    /**
     * 应用名称
     */
    @Value("${spring.application.name}")
    private String applicationName;

    private String serverAddr;

    private String dNamespace;


    @Autowired
    public void init() {
        serverAddr = "ip:8848";
        dNamespace = DEFAULT_NAMESPACE;
        initTip(null);
        initTip(Locale.CHINA);
        initTip(Locale.US);
        log.info("初始化系统参数成功!应用名称:{},Nacos地址:{},提示语命名空间:{}", applicationName, serverAddr, dNamespace);
    }

    private void initTip(Locale locale) {
        String content = null;
        String dataId = null;
        ConfigService configService = null;
        try {
            if (locale == null) {
                dataId = baseName + ".properties";
            } else {
                dataId = baseName + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
            }
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
            properties.put(PropertyKeyConst.NAMESPACE, dNamespace);
            configService = NacosFactory.createConfigService(properties);
            content = configService.getConfig(dataId, DEFAULT_GROUP, 5000);
            if (StringUtils.isEmpty(content)) {
                log.warn("配置内容为空,跳过初始化!dataId:{}", dataId);
                return;
            }
            log.info("初始化国际化配置!配置内容:{}", content);
            saveAsFileWriter(dataId, content);
            setListener(configService, dataId, locale);
        } catch (Exception e) {
            log.error("初始化国际化配置异常!异常信息:{}", e);
        }
    }

    private void setListener(ConfigService configService, String dataId, Locale locale) throws com.alibaba.nacos.api.exception.NacosException {
        configService.addListener(dataId, DEFAULT_GROUP, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                log.info("接收到新的国际化配置!配置内容:{}", configInfo);
                try {
                    initTip(locale);
                } catch (Exception e) {
                    log.error("初始化国际化配置异常!异常信息:{}", e);
                }
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });
    }

    private void saveAsFileWriter(String fileName, String content) throws FileNotFoundException {
        String projectPath = ResourceUtils.getURL("classpath:").getPath();

        String oldPath = System.getProperty("user.dir");

        log.info("============================");
        log.info(projectPath);
        log.info(oldPath);
        log.info("============================");

        String path = projectPath + File.separator + baseFolder;
        try {
            fileName = path + File.separator + fileName;
            File file = new File(fileName);
            FileUtils.writeStringToFile(file, content);
            log.info("国际化配置已更新!本地文件路径:{}", fileName);
        } catch (IOException e) {
            log.error("初始化国际化配置异常!本地文件路径:{}异常信息:{}", fileName, e);
        }
    }


    @Autowired
    private ConfigurableApplicationContext applicationContext;

    private static final String DEFAULT_GROUP = "DEFAULT_GROUP";

    private static final String DEFAULT_NAMESPACE = "758eb78f-aae6-4393-96e4-a3ed5e8111c2";

}

nacos这里是参考了一篇博客
Nacos实现SpringBoot国际化的增强

这篇博客里面的messageSource 使用的是ResourceBundleMessageSource,但是据说这个类不能更新配置文件。
同时,这个博客存储配置文件的时候有个坑,path这里得设置成classpath:i18n/exception
最后膜拜一下大佬。

顺便吐槽一下,nacos是真的好用,但是文档太简略了,也可能是我太菜了,汗。

我用的是ReloadableResourceBundleMessageSource ,配置文件缓存设置了一秒,
如果线上使用,考虑效率要设置的久一点。

@Configuration
public class SpringConfig {
    @Bean
    protected MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();

        messageSource.setBasename("classpath:i18n/exception");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(1);

        return messageSource;
    }
}

至此,我们修改nacos相关的配置文件。NacosConfig类里面设置的监听器就会接收到信号。

然后将最新的文件从nacos配置中心读取到,再覆盖掉本地的配置文件。

等到messageSource设置的缓存时间到期,就会把新的配置文件读取到缓存中。

完美!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值