SpringBoot微服务项目异常国际化

1. 前言

异常处理是任何软件开发项目的重要组成部分,包括 Spring Boot 微服务。当出现错误时,提供有意义且用户友好的错误信息对于帮助用户理解和解决问题至关重要。在本博客文章中,我们将探讨如何在 Spring Boot 微服务项目中实现异常国际化。

注意:本文是以前后端分离为前提,单纯的给后端springboot项目做异常输出国际化。

2. 为什么要做国际化

异常国际化允许我们根据用户的语言环境以不同的语言呈现错误信息。通过支持多种语言,我们可以为全球用户提供更好的用户体验。不再仅仅使用单一语言显示错误信息,而是根据用户的首选语言动态翻译,使用户更容易理解和解决问题。

3. 最终目标

  1. 实现validation注解校验异常信息国际化
  2. 自定义异常信息国际化

4. 思路

  1. 使用validation-api的校验注解对需要校验的参数进行标注,并将错误提示信息放入国际化资源文件中,spring mvc已经支持了具体的实现,默认的校验国际化文件名是“ValidationMessages”放到resources目录下即可,自定义异常单独处理并设置国际化资源文件位置。
  2. 自定义业务异常枚举及异常处理工具类
  3. 编写全局异常处理类,处理各种异常及返回错误信息
  4. 根据请求头header的参数“Lang”传递的语言设置Locale。
  5. 编写国际化资源文件,包含错误码code和错误信息。
  6. 编写测试接口,分别测试注解校验异常和自定义业务异常国际化结果。

5. 实现步骤:

5.1 添加pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.10</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.star95</groupId>
  <artifactId>springboot-i18n</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>springboot-i18n</name>
  <description>springboot-i18n</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- mvc -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- validation校验依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>

    <!-- knife4j -->
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

这里主要添加了spring-boot-starter-web用来实现MVC,spring-boot-starter-validation引入hibernate-validator的具体校验实现。

5.2 自定义业务异常以及全局异常处理

自定义业务异常,字段主要是错误码以及错误信息。

package com.star95.springbooti18n.exception;

import com.star95.springbooti18n.common.constants.MessagesEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 业务逻辑异常 Exception
 */

@Data
@EqualsAndHashCode(callSuper = true)
public final class BusinessException extends RuntimeException {

    /**
     * 业务错误码
     */

    private String code;
    /**
     * 错误提示
     */

    private String message;

    /**
     * 空构造方法,避免反序列化问题
     */

    public BusinessException() {
    }

    public BusinessException(MessagesEnum errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }

    public BusinessException(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public BusinessException setCode(String code) {
        this.code = code;
        return this;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public BusinessException setMessage(String message) {
        this.message = message;
        return this;
    }

}

全局异常处理器,用来处理抛出的各类异常。

package com.star95.springbooti18n.exception;

import com.google.common.collect.Lists;
import com.star95.springbooti18n.dto.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;

/**
 * 异常处理器
 */

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常
     */

    @ExceptionHandler(BusinessException.class)
    public Result<?> handleServiceException(BusinessException e
{
        log.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }

    /**
     * 处理对象类型参数校验异常
     *
     * @param e
     * @return
     */

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result<StringhandleMethodArgumentNotValidException(MethodArgumentNotValidException e
{
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> errors = Lists.newArrayList();
        for (FieldError error : fieldErrors) {
            errors.add(error.getDefaultMessage());
        }
        String error = StringUtils.join(errors, " | ");
        log.info("MethodArgumentNotValidException:{}", error);
        return Result.error(error);
    }

    /**
     * 处理基础类型参数校验异常
     *
     * @param ex
     * @return
     */

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result handleConstraintViolationException(ConstraintViolationException ex
{
        Set<ConstraintViolation<?>> ConstraintViolations = ex.getConstraintViolations();
        List<String> errors = Lists.newArrayList();
        for (ConstraintViolation constraint : ConstraintViolations) {
            errors.add(constraint.getMessage());
        }
        String errorMsg = StringUtils.join(errors, " | ");
        log.info("ConstraintViolationException:{}", errorMsg);
        return Result.error(errorMsg);
    }

    /**
     * 处理顶层异常Exception
     *
     * @param e
     * @return
     */

    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e
{
        log.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }
}

5.3 定义异常枚举及处理工具类

所有业务异常通过统一的异常枚举MessagesEnum列出,后续通过code去对应的国际化资源文件中找对应的错误信息。

package com.star95.springbooti18n.common.constants;

import lombok.Getter;

/**
 * 错误码枚举类
 */

public enum MessagesEnum {
    CAPTCHA_CHECKED_ERROR("captcha_checked_error""验证码错误"),
    USERNAME_PASSWORD_INCORRECT("username_password_incorrect""用户名或密码错误"),
    ;

    @Getter
    private String code;

    @Getter
    private String msg;

    MessagesEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

再写一个根据异常枚举获取异常信息的工具类ExceptionUtil,方便在业务逻辑中调用。

package com.star95.springbooti18n.exception;

import com.star95.springbooti18n.common.constants.MessagesEnum;
import com.star95.springbooti18n.common.util.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.util.StringUtils;

import java.text.MessageFormat;

/**
 * 异常工具类
 *
 */

@Slf4j
public class ExceptionUtil {
    public static BusinessException exception(MessagesEnum errorCode) {
        String msg = MessageUtils.getMessage(errorCode.getCode(), null);
        if (StringUtils.isEmpty(msg)) {
            msg = errorCode.getMsg();
        }
        return exception0(errorCode.getCode(), msg);
    }

    public static BusinessException exception(MessagesEnum errorCode, Object... params) {
        String msg = MessageUtils.getMessage(errorCode.getCode(), params);
        if (StringUtils.isEmpty(msg)) {
            MessageFormat format = new MessageFormat(errorCode.getMsg(), LocaleContextHolder.getLocale());
            msg = format.format(params);
        }
        return exception0(errorCode.getCode(), msg);
    }

    public static BusinessException exception0(String code, String message) {
        return new BusinessException(code, message);
    }
}

MessageUtils中注入了MessageSource,主要是用来从国际化资源文件中查找对应code的错误信息。

package com.star95.springbooti18n.common.util;

import com.star95.springbooti18n.common.constants.MessagesEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.text.MessageFormat;

/**
 * 国际化支持工具类
 */

@Component
public class MessageUtils {

    private static MessageSource messageSource;

    @Autowired
    public void setMessageSource(MessageSource messageSource1) {
        messageSource = messageSource1;
    }

    public static String getMessage(String code) {
        return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
    }

    public static String getMessage(String code, Object... arg) {
        return messageSource.getMessage(code, arg, LocaleContextHolder.getLocale());
    }

    public static String getI18NMessage(MessagesEnum messagesEnum) {
        return getI18NMessage(messagesEnum.getCode(), messagesEnum.getMsg());
    }

    public static String getI18NMessage(String code, String defaultMsg) {
        String msg = getMessage(code);
        if (StringUtils.isEmpty(msg)) {
            return defaultMsg;
        }
        return msg;
    }

    public static String getI18NMessage(String code, String defaultMsg, Object... arg) {
        String msg = getMessage(code, arg);
        if (StringUtils.isEmpty(msg)) {
            MessageFormat format = new MessageFormat(defaultMsg, LocaleContextHolder.getLocale());
            msg = format.format(arg);
        }
        return msg;
    }
}


5.4 配置类

WebMvcConfiguration类是一个配置类,里面定义了LocaleResolver对象,是一个自定义的I18NLocaleResolver对象,注意:方法名必须是localeResolver,用来覆盖spring默认装配的对象。

package com.star95.springbooti18n.config;

import com.star95.springbooti18n.common.util.I18NLocaleResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    /**
     * 获取LocaleResolver
     *
     * @return
     */

    @Bean
    public LocaleResolver localeResolver() {
        return new I18NLocaleResolver();
    }
}

I18NLocaleResolver主要用来获取Locale的,基本上实现resolveLocale这个方法即可。本文采用从请求头header里去获取Lang这个参数,从前端调接口传具体的语言,这也是实现国际化的关键所在,如果没有传值则取默认Locale,传了语言就会使用创建的new Locale(language)对象。注意:如果传递一个无效或者国际化资源中不存在的语言,则会去默认的国际化资源文件中找。

package com.star95.springbooti18n.common.util;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class I18NLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        //获取请求中的语言参数
        String language = httpServletRequest.getHeader("Lang");
        //如果没有就使用默认的(根据主机的语言环境生成一个 Locale )。
        Locale locale = Locale.getDefault();
        //如果请求的链接中携带了 国际化的参数
        if (StringUtils.hasText(language)) {
            //国家,地区
            locale = new Locale(language);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

5.5 国际化资源文件

SpringBoot微服务项目异常国际化validation校验国际化资源默认是放在resources目录下的ValidationMessages这个文件名里,如果要更改文件名字或路径需要重新定义Validator这个bean。自定义的国际化文件需要指定文件路径,比如通过yml的配置,指定basename即可,这里我写的文件名是messages,你也可以换成其他的名字。

spring:
  messages:
    basename: i18n/messages
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

这里的文件名是按照{basename}_{language}.properties的固定格式。默认的文件就是{basename}.properties。比如这里我的validation注解校验的异常文件英文的就是ValidationMessages_en.properties,自定义的国际化文件英文的就是messages_en.properties。ValidationMessages_zh.properties文件内容如下:

customer_account_not_empty = 账号不能为空
password_not_empty = 密码不能为空
captcha_not_empty = 验证码不能为空
captcha_length_error = 验证码长度必须在{min}到{max}之间
captchaKey_not_null = 验证码key不能为空
age_min_error = 年龄最小值为0
id_max_error = id不能超过{value}

ValidationMessages_en.properties文件内容如下:

customer_account_not_empty = Account cannot be empty.
password_not_empty = Password cannot be empty.
captcha_not_empty = Captcha cannot be empty.
captcha_length_error = Captcha length must be between {min} and {max}.
captchaKey_not_null = Captcha key cannot be null.
age_min_error = The minimum age is 0
id_max_error = id cannot exceed {value}

messages_zh.properties文件内容如下:

username_password_incorrect = 用户名或密码错误

messages_en.properties文件内容如下:

username_password_incorrect = customerAccount or password is incorrect

默认的国际化文件内容我这里是和zh中文文件内容相同,不再列出。

5.6 示例接口

示例接口以客户端接口为例。客户端登录接口login接收的是对象类型的参数,添加了@Validated校验注解,参数是一个CustomerUserDto对象,对象里的属性会按照校验注解进行校验,然后通过ValidationMessages获取异常提示信息。获取客户信息getCustomerInfo接口接收的是一个基本类型的参数,也添加了校验注解。注意在方法参数上不要添加@Validated,不生效,需要添加到类上才会生效。

package com.star95.springbooti18n.controller;

import com.star95.springbooti18n.dto.CustomerUserDto;
import com.star95.springbooti18n.dto.Result;
import com.star95.springbooti18n.exception.ExceptionUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.Max;
import java.util.UUID;

import static com.star95.springbooti18n.common.constants.MessagesEnum.USERNAME_PASSWORD_INCORRECT;

/**
 * 客户端接口
 */

@Slf4j
@RestController
@RequestMapping("/customer")
@Api(tags = "客户端接口")
@Validated
public class CustomerController {
    @PostMapping("/login")
    @ApiOperation("客户端登录")
    public Result<String> login(@Validated @RequestBody CustomerUserDto customerUserDto) {
        log.info("客户端登录请求参数,customerUserDto:{}", customerUserDto);
        if (!"admin".equals(customerUserDto.getCustomerAccount())) {
            throw ExceptionUtil.exception(USERNAME_PASSWORD_INCORRECT);
        }
        log.info("用户账号:{},登录成功!", customerUserDto.getCustomerAccount());
        String token = UUID.randomUUID().toString();
        return Result.ok(token);
    }

    @PostMapping("/getCustomerInfo")
    @ApiOperation("获取客户信息")
    public Result<CustomerUserDto> getCustomerInfo(@RequestParam("id") @Max(value = 9999, message = "{id_max_error}") int id) {
        log.info("客户端获取客户信息请求参数,id:{}", id);
        CustomerUserDto customerUserDto = new CustomerUserDto();
        customerUserDto.setCustomerAccount("1111");
        return Result.ok(customerUserDto);
    }
}

CustomerUserDto对象的属性添加了校验注解,注解的message里是以{code}这种形式编写,会自动查找对应国际化资源文件ValidationMessages里code对应的错误信息。比如基本的“{customer_account_not_empty}”,会对应国际化资源文件比如英文:customer_account_not_empty = Account cannot be empty. 另外还支持动态参数配置,比如:”{captcha_length_error}”,会对应国际化资源文件比如英文:captcha_length_error = Captcha length must be between {min} and {max}. 这里它的{min}和{max}会自动获取注解上的相同参数名的值。

package com.star95.springbooti18n.dto;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Data
@NoArgsConstructor
public class CustomerUserDto {
    /**
     * 账号
     */

    @NotEmpty(message = "{customer_account_not_empty}")
    private String customerAccount;

    /**
     * 密码
     */

    @NotEmpty(message = "{password_not_empty}")
    private String password;

    /**
     * 验证码
     */

    @NotEmpty(message = "{captcha_not_empty}")
    @Length(min = 1, max = 4, message = "{captcha_length_error}")
    private String captcha;

    /**
     * 验证码key
     */

    @NotNull(message = "{captchaKey_not_null}")
    private String captchaKey;

    @Min(value = 0, message = "{age_min_error}")
    private int age;
}

6. 效果演示

下面通过swagger演示一下校验国际化效果:

注意:在swagger默认是不支持动态参数的,请求头部不能输入参数,如果需要在请求头部设置参数,在文档管理菜单下的个性化设置里勾选上“开启动态请求参数”这一项即可。

SpringBoot微服务项目异常国际化

6.1 Validation参数校验异常

对象类型的参数加了校验注解,异常时抛出的是MethodArgumentNotValidException。

  • 默认情况直接调用(请求头不传“Lang”参数)

SpringBoot微服务项目异常国际化这里默认是中文环境下,所有的校验都生效了,并且错误提示都使用了中文国际化资源文件的内容,验证码错误的国际化资源文件内容是:“captcha_length_error = 验证码长度必须在{min}到{max}之间”,输出的结果是:“验证码长度必须在1到4之间”,可以看到获取到了注解上的min和max这两个属性参数的值。

  • Lang传英文语言,在请求头设置Lang=en

SpringBoot微服务项目异常国际化还是刚才的参数,可以看到输出结果取了国际化en文件里的内容。SpringBoot微服务项目异常国际化

  • 输入一个在项目里不存在的国际化语言,Lang=th

SpringBoot微服务项目异常国际化这里我们看到输出了中文的错误提示,是因为国际化没有获取到指定的资源时,会从默认的文件里取值,也就是从ValidationMessages.properties这个文件中读取错误码。

6.2 自定义异常

自定义的异常时抛出的是自定义的异常类BusinessException。

  • 默认情况下,请求头不输入Lang参数,然后基本参数都输入正确,让Validation注解校验通过,进入业务异常,也就是customerAccount字段的值不输入“admin”就会触发抛出业务异常,获取的是中文异常提示。
SpringBoot微服务项目异常国际化

  • Lang传英文语言,在请求头设置Lang=en

还是刚才的基本参数校验通过,在设置了Lang=en英文语言参数后,错误提示获取到了英文的国际化资源内容。SpringBoot微服务项目异常国际化

6.3 基本类型参数的校验异常

另外需要注意的是关于基本类型参数的校验,这里特意写了一个根据id获取客户信息的接口,在接口类上加了@Validated注解,它也会正常触发对应国际化资源的错误提示,但是它的异常是ConstraintViolationException,对应GlobalExceptionHandler类里的加了@ExceptionHandler(ConstraintViolationException.class)这个注解的方法处理。

@PostMapping("/getCustomerInfo")
@ApiOperation("获取客户信息")
public Result<CustomerUserDto> getCustomerInfo(@RequestParam("id") @Max(value = 9999, message = "{id_max_error}") int id) {
    log.info("客户端获取客户信息请求参数,id:{}", id);
    CustomerUserDto customerUserDto = new CustomerUserDto();
    customerUserDto.setCustomerAccount("1111");
    return Result.ok(customerUserDto);
}

SpringBoot微服务项目异常国际化SpringBoot微服务项目异常国际化

7. 总结

其实SpringBoot国际化是由Spring MVC的能力做支撑,只不过使用和配置相对简单了,我们只需要做国际化语言的处理即可。对于注解校验的国际化只需去编写国际化资源文件的内容,对于自定义异常需要自己编写异常处理的逻辑,然后从国际化资源文件中获取错误信息返回即可。不管采用何种方式去做国际化,实质基本都是通过不同的Locale获取不同的国际化资源文件里的具体信息。

另外本文只是对于后端的异常国际化做了解读,其实针对于前后端接口交互数据的国际化大多数也是通过获取语言然后读取不同语言对应的数据来完成,关于不同语言的数据存储方案也有很多,感兴趣的可自行搜索。本文中涉及的demo已上传gitee。

Gitee地址:https://gitee.com/star95/springboot-i18n.git

欢迎关注公众号,欢迎分享、点赞、在看

SpringBoot微服务项目异常国际化


原文始发于微信公众号(小新成长之路):SpringBoot微服务项目异常国际化

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/238281.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!