springboot整合Dubbo异常和多语言处理

Ccframe采用标准的spring data i18n方案。在处理多语言异常时,做了一些针对性的处理。包括以下几个方面:

多语言支持

引入LocalConfig,设置默认的语言,指定i18n properties的位置:

package org.ccframe.app;

import org.ccframe.config.GlobalEx;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

import java.util.Locale;

@Configuration
public class LocaleConfig {

    @Value("${app.default-ocale:zh-CN}") // 默认语言
    String defaultLocale;

    /**
     * 默认解析器 其中locale表示默认语言
     */
    @Bean
    public LocaleContextResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver();
        resolver.setDefaultLocale(Locale.forLanguageTag(defaultLocale));
        resolver.setCookieName(GlobalEx.CCFRAME_LOCALE);
        resolver.setCookieMaxAge(Integer.MAX_VALUE);
        return resolver;
    }

}

我们可以通过application-dev.properties设置app.default-locale指定异常的默认多语言,默认是中文(zh-CN)
由于是微服务系统,采用session记录就不太合适了,因此,采用了cookie来针对locale进行记录,默认的cookie key是ccframeLocale

多语言切换

添加一个controller方法进行切换:

    @GetMapping("locale") //多语言切换
    public ResponseEntity<Void> locale(String lang, @ApiIgnore HttpServletRequest request, @ApiIgnore HttpServletResponse response) {
        localeContextResolver.resolveLocaleContext(request);
        localeContextResolver.setLocale(request, response, Locale.forLanguageTag(lang));
        return new ResponseEntity<Void>(HttpStatus.OK);
    }

这样前端可以通过该API调用实现语言切换

多语言异常

异常主要定义两种,一种是业务异常,一种是系统异常。

业务异常

BusinessException:

public BusinessException(String msgKey, Object[] args){
    this.msgKey = msgKey ;
    this.args = args;
}

主要是msgKey和args

msgKey为properties文件里的key,而args为带入的参数。为了开发方便,定义了一个原样输出的msgKey:

# -- direct output message --
message={0}

系统异常

由于整合了dubbo,当dubbo无法转换异常时,会将异常包装为一个RuntimeException输出,同样,Lobok的@SneakyThrows也会干同样的活。因此,在@RestControllerAdvice捕获输出时,实际是无法捕获到原始的异常信息的。但是有一个公共的特点,就是异常的message会包含异常类的原始信息,例如
org.springframework.orm.ObjectOptimisticLockingFailureException: oh my god; nested exception is java.lang.Exception: shit\r\norg.springframework.orm.ObjectOptimisticLockingFailureException: oh my god; nested exception is java.lang.Exception: shit\r\n\tat org.ccframe.subsys.core.service.ParamService.testException(ParamService.java:46)\r\n\tat org.ccframe.subsys.core.service.ParamService$$FastClassBySpringCGLIB

也就是ObjectOptimisticLockingFailureException实际上包含了该异常文本,因此我们不需要具体的异常类,只需根据异常的文本进行转换,就得到了多语言的properties key

通过一个正则提取了异常的前部分,并根据异常名称进行国际化处理:

    /**
     * 提取异常类名的正则.
     */
    private static final Pattern CLASS_PATTERN = Pattern.compile("^([a-z]+(\\.[a-z_][a-z0-9_]*)*\\.[A-Za-z0-9_]*Exception): ");

具体规则实现的逻辑:
1)当异常为BusinessException时,根据BusinessException的msgKey找到国际化资源,并根据args带入占位符参数。该异常打包进API jar,因此可以被Service Interface处理。

2)当异常为Trowable时(例如包装过的RuntimeException),先尝试从国际化里查找key,如果找到了,根据国际化返回一个对应的内容。当找不到时,输出错误Throwable的message

3)错误最终输出Result对象,message为异常提示的内容,而result部分则为错误的原始信息,包括国际化的key,或者Exception的全名称(当没有找到国际化key时,使用全名)。这样前端可以根据返回不同的异常类型,分支处理对应的逻辑代码

具体实现类如下:

package org.ccframe.commons.mvc;

import com.alibaba.fastjson2.JSON;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.ccframe.commons.base.BusinessException;
import org.ccframe.commons.filter.CcRequestLoggingFilter;
import org.ccframe.config.GlobalEx;
import org.ccframe.subsys.core.dto.Result;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
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.orm.ObjectOptimisticLockingFailureException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RestControllerAdvice
@Log4j2
public class GlobalRestControllerAdvice implements ResponseBodyAdvice<Object> {

    private MessageSource messageSource; //国际化资源

    private LocaleResolver localeResolver;

    private static final Pattern CONTROLLER_PATTERN = Pattern.compile("^org\\.ccframe\\.(subsys|sdk)\\.[a-z0-9]+\\.controller\\. ");

    private Object[] EMPTY_ARGS = new Object[0];

    /**
     * 提取异常类名的正则.
     */
    private static final Pattern CLASS_PATTERN = Pattern.compile("^([a-z]+(\\.[a-z_][a-z0-9_]*)*\\.[A-Za-z0-9_]*Exception): ");

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return CONTROLLER_PATTERN.matcher(returnType.getContainingClass().getName()).find();    // 只有自己的cotroller类才需要进入,否则swagger都会挂了
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        System.out.println(returnType.getContainingClass());
        Result<Object> result = new Result<>();
        result.setResult(body);
        result.setCode(HttpStatus.SC_OK);

        if(returnType.getParameterType() == String.class){ //String返回要特殊处理
            return JSON.toJSONString(result);
        }else {
            return result;
        }
    }

    public GlobalRestControllerAdvice(MessageSource messageSource, LocaleResolver localeResolver){
        this.messageSource = messageSource;
        this.localeResolver = localeResolver;
    }

    private Result<String> createError(HttpServletRequest request, Throwable tr,int code, String msgKey, Object[] args){
        Locale currentLocale = localeResolver.resolveLocale(request);
        String message = "";
        try {
            message = messageSource.getMessage(msgKey, args, currentLocale);
        }catch (NoSuchMessageException ex){
            message = tr.getMessage();
        }finally {
            if(tr instanceof BusinessException){ //业务异常采用简短日志
                log.error(message);
            }else{
                log.error(message, tr); //其它异常采用详细日志
            }

            CcRequestLoggingFilter.pendingLog(); //服务器可以记录出错时的请求啦😂
        }
        String messageReturn = msgKey;
        if(GlobalEx.ORIGIN_MESSAGE_KEY.equals(msgKey)){   //模板输出时,msgKey为异常类名
            messageReturn = tr.getClass().getSimpleName(); //JDK异常直接记录异常名称,否则记录模板key
        }

        return Result.error(code, message, messageReturn);
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    public Result<?> handlerNoFoundException(HttpServletRequest request, Exception e) {
        return createError(request, e, HttpStatus.SC_NOT_FOUND, "error.mvc.uriNotFound", EMPTY_ARGS);
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result<?> httpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException e){
        return createError(request,e, HttpStatus.SC_NOT_FOUND,"error.mvc.methodNotSupported",
            new Object[]{e.getMethod(), StringUtils.join(e.getSupportedMethods(), GlobalEx.DEFAULT_TEXT_SPLIT_CHAR)});
    }

    @ExceptionHandler(BusinessException.class)
    public Result<?> businessException(HttpServletRequest request, BusinessException e){
        return createError(request,e, HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMsgKey(), e.getArgs());
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class) // 文件上传超限,nginx请设置为10M
    public Result<?> handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException e) {
        return createError(request, e, HttpStatus.SC_INTERNAL_SERVER_ERROR, "error.mvc.fileTooLarge", EMPTY_ARGS);
    }

    @ExceptionHandler(Throwable.class)
    public Result<?> handleException(HttpServletRequest request, Throwable tr) {
        Matcher matcher = CLASS_PATTERN.matcher(tr.getMessage());
        String resultStr = tr.getClass().getName(); // 默认是异常的message
        if(matcher.find()){
            resultStr = matcher.group(1); //如果以class形式开头,则按照class名找i18n资源
        }
        return createError(request,tr, HttpStatus.SC_INTERNAL_SERVER_ERROR, resultStr, EMPTY_ARGS);
    }
}


对应的语言资源文件:

相关推荐

  1. Springboot整合hibernate validator 全局异常处理

    2024-03-16 05:16:04       44 阅读
  2. dubbo 统一异常处理

    2024-03-16 05:16:04       32 阅读
  3. springboot全局异常处理自定义异常处理

    2024-03-16 05:16:04       66 阅读
  4. SpringBoot异常处理单元测试

    2024-03-16 05:16:04       57 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-16 05:16:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-16 05:16:04       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-16 05:16:04       82 阅读
  4. Python语言-面向对象

    2024-03-16 05:16:04       91 阅读

热门阅读

  1. 申请软著提交的演示视频有什么要求

    2024-03-16 05:16:04       38 阅读
  2. vue3 ref 和 reactive 区别

    2024-03-16 05:16:04       35 阅读
  3. Arcade官方教程解析8 Multiple Levels and Other Layers

    2024-03-16 05:16:04       40 阅读
  4. AJAX学习日记——Day 2

    2024-03-16 05:16:04       41 阅读
  5. Android 启动service(Kotlin)

    2024-03-16 05:16:04       29 阅读
  6. 俄罗斯方块游戏开发思路随想

    2024-03-16 05:16:04       37 阅读
  7. android api 34 编译ffmpeg with libfdk-aac

    2024-03-16 05:16:04       39 阅读
  8. 基于arm的ubuntu上运行qgc

    2024-03-16 05:16:04       38 阅读
  9. 程序员面试—反问示例

    2024-03-16 05:16:04       40 阅读
  10. Android平台架构和Android Framework的区别

    2024-03-16 05:16:04       49 阅读
  11. 现代 Android 开发的第一步Kotlin

    2024-03-16 05:16:04       44 阅读