springboot 项目中实战演练,功能有参数非空校验、默认国际化、正常返回参数国际化、抛异常后信息国际化、全局异常后继续切面拦截国际化、动态参数国际化等后端各个方面的实战。
1、新建springboot微服务,pom中jar的基本引用
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.14.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2、yml配置文件;
spring:
thymeleaf:
encoding=UTF-8
content-type=text/html
cache=false
mode=LEGACYHTML5
messages:
encoding: UTF-8
cache-seconds: 1
basename: static/i18n/messages
3、properties的创建:
引入配置信息:比如英文版
demo.key.null= key not null !!!
1000=hello{0}, your verification code is: {1}
4、WebMvcConfigurationSupport 类中接口的重写:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @author wanghuainan
* @date 2021/10/13
* 在URL后面加参数,并且使用默认国际化校验时不需要此类 (如果自定义国际化配置文件一定需要此类)?lang=zh_CN/?lang=zh_US;
* 如果后端通过请求头接参数:accept-language:zh-CN/en ,则需要调此类,如果不加此类报错:"resultMsg": "{demo.key.null}"(使用默认国际化校验时不需要此类);即上下可能是互斥的
*/
@Configuration
public class ValidatorConfiguration extends WebMvcConfigurationSupport {
@Autowired
private MessageSource messageSource;
@Override
public Validator getValidator() {
return validator();
}
@Bean
public Validator validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
}
5、全局异常的创建:
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AppExceptionHandler {
@Autowired
MessageSource messageSource;
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Object methodArgumentNotValid(HttpServletRequest req, MethodArgumentNotValidException ex) {
// Request r= new Request();源码所示
//ResultBean result = ResultBean.FAIL;
ResponseDTO result = new ResponseDTO();
FieldError fe = ex.getBindingResult().getFieldError();
String msg = fe.getDefaultMessage();
Locale locale = LocaleContextHolder.getLocale();
// LocaleContextHolder.setLocale(Locale.ENGLISH);
LocaleContextHolder.setLocale(Locale.US);
locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage(fe, LocaleContextHolder.getLocale());
//String message = messageSource.getMessage(fe, Locale.ENGLISH);
log.info("---MethodArgumentNotValidException Handler--- ERROR: {}",msg);
result.setResultMsg(msg);
// List<ObjectError> errors =ex.getBindingResult().getAllErrors();
// StringBuffer errorMsg=new StringBuffer();
// errors.stream().forEach(x -> errorMsg.append(x.getDefaultMessage()).append(";"));
// log.error("---MethodArgumentNotValidException Handler--- ERROR: {}", errorMsg.toString());
// result.setResultMsg(errorMsg.toString());
return result;
}
/**
* 自定义异常
*/
@ExceptionHandler(Exception.class)
public ResponseDTO handleMgtInvalidParamException(Exception e, HttpServletRequest request) {
ResponseDTO r = new ResponseDTO(ResponseCodeEnum.FAILED,null);
return r;
}
}
6、切面 接口 ResponseBodyAdvice<Object> 的实现:
import com.nandao.i18n.dto.ResponseDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
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.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.servlet.support.RequestContext;
import javax.servlet.http.HttpServletRequest;
/**
* 这里同时拦截控制层和全局异常类
*/
@ControllerAdvice(basePackages={"com.nandao.i18n.controller","com.nandao.i18n.exception"})
public class I18nResponseBodyAdvice implements ResponseBodyAdvice<Object>{
private MessageSource messageSource;
@Value("${spring.messages.basename}")
private String basename;
@Value("${spring.messages.cache-seconds}")
private long cacheMillis;
@Value("${spring.messages.encoding}")
private String encoding;
protected Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Object beforeBodyWrite(Object obj, MethodParameter method,
MediaType type, Class<? extends HttpMessageConverter<?>> converter,
ServerHttpRequest request, ServerHttpResponse response) {
try {
if(obj instanceof ResponseDTO){
ResponseDTO result = (ResponseDTO) obj;
String resultCode = result.getResultCode();
if(resultCode!= null){
HttpServletRequest req = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String i18nMsg = getMessage(req, resultCode);
if(null !=i18nMsg){
result.setResultMsg(i18nMsg);
}else {
log.info("采用上游的赋值");
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("返回值国际化拦截异常",e);
}
return obj;
}
@Override
public boolean supports(MethodParameter arg0,
Class<? extends HttpMessageConverter<?>> arg1) {
return true;
}
/**
* 返回国际化的值
* @param request
* @param key
* @return
*/
public String getMessage(HttpServletRequest request, String key){
RequestContext requestContext = new RequestContext(request);
String value = requestContext.getMessage(key);
return value;
}
/**
* 初始化
* @return
*/
private MessageSource initMessageSource() {
ReloadableResourceBundleMessageSource messageSource=new ReloadableResourceBundleMessageSource();
log.info("baseName====>:" + this.basename);
messageSource.setBasename(basename);
messageSource.setDefaultEncoding(encoding);
messageSource.setCacheMillis(cacheMillis);
return messageSource;
}
/**
* 设置当前的返回信息,也可以使用
* @param request
* @param code
* @return
*/
/*public String getMessage(HttpServletRequest request,String code){
if(messageSource==null){
messageSource=initMessageSource();
}
Request r = new Request();
String lauage=request.getHeader("accept-language");
//默认没有就是请求地区的语言
Locale locale=null;
if(lauage==null){
locale=request.getLocale();
}
else if("en".equals(lauage)){
locale=Locale.ENGLISH;
}
else if("ko".equals(lauage)){
locale=Locale.KOREAN;
}
else if("zh-CN".equals(lauage)){
locale=Locale.CHINA;
}
//其余的不正确的默认就是本地的语言
else{
locale=request.getLocale();
}
String result=null;
try {
result = messageSource.getMessage(code, null, locale);
//result = messageSource.getMessage(code, null, "en_US");
} catch (NoSuchMessageException e) {
log.error("Cannot find the error message of internationalization, return the original error message.");
}
if(result==null){
return code;
}
return result;
}
*/
}
7、aspect切面的实现和6类似,二选一方案:
import com.nandao.i18n.constant.ResultEnglishMsg;
import com.nandao.i18n.dto.ResponseDTO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;
/**
* @author wanghuainan
* @date 2021/10/9
*/
@Aspect
@Slf4j
@Component
public class GlobalEnglishMsgAspect {
private MessageSource messageSource;
@Value("${spring.messages.basename}")
private String basename;
@Value("${spring.messages.encoding}")
private String encoding;
@Pointcut("execution(public * com.nandao.i18n.controller.*.*(..))")
private void pointCut() {
}
@Pointcut("execution(public * com.nandao.i18n.exception.*.*(..))")
private void pointExceptionCut() {
}
/**
* 业务接口请求后统一处理错误状态码
* @param joinPoint
* @param returnVal
*/
@AfterReturning(returning = "returnVal", pointcut = "pointCut()")
public void doAftereReturning(JoinPoint joinPoint, Object returnVal) {
commonReturning( joinPoint, returnVal);
}
/**
* 业务接口请求异常后统一处理错误状态码
* @param joinPoint
* @param returnExceptionVal
*/
@AfterReturning(returning = "returnExceptionVal", pointcut = "pointExceptionCut()")
public void doAftereExceptionReturning(JoinPoint joinPoint, Object returnExceptionVal) {
commonReturning( joinPoint, returnExceptionVal);
}
private void commonReturning(JoinPoint joinPoint, Object returnCommonVal) {
String classMethod = null;
if (returnCommonVal instanceof ResponseDTO) {
ResponseDTO r = (ResponseDTO) returnCommonVal;
/**
* 如果返回的是错误状态码,则需要判断是否需要转化成英文
*/
String returnCode = r.getResultCode();
if (!returnCode.equals("100000")) {
classMethod = getClassMethodName(joinPoint);
boolean flag = checkHeaderName();
if (!flag) {
log.info("此接口[{}]返回的msg不需要转化为英文,,错误码[{}]", classMethod,returnCode);
return;
}
if (ResultEnglishMsg.codeList.contains(returnCode)) {
log.info("此接口[{}]特殊处理英文msg,错误码[{}]", classMethod,returnCode);
return;
}
log.info("此接口[{}]返回的msg需要转化为英文,,错误码[{}]", classMethod,returnCode);
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String i18nMsg = getMessage(req, returnCode);//获取国际化配置
/**
* 英文提升如果为空,则还用原来的中文提升
*/
if (Objects.nonNull(i18nMsg)) {
r.setResultMsg(i18nMsg);
} else {
log.info("英文提升为空,还用原来的中文提升,错误码code:[{}],接口名[{}]", returnCode, classMethod);
}
}
} else {
classMethod = getClassMethodName(joinPoint);
log.info("此接口[{}]返回的不是R对象,没有msg,不需要英文转化", classMethod);
}
}
/**
* 初始化
* @return
*/
private MessageSource initMessageSource() {
ReloadableResourceBundleMessageSource messageSource=new ReloadableResourceBundleMessageSource();
log.info("baseName====>:" + this.basename);
messageSource.setBasename(basename);
messageSource.setDefaultEncoding(encoding);
return messageSource;
}
/**
* 设置当前的返回信息
* @param request
* @param code
* @return
*/
public String getMessage(HttpServletRequest request,String code){
if(messageSource==null){
messageSource=initMessageSource();
}
String result=null;
try {
result = messageSource.getMessage(code, null, request.getLocale());
} catch (NoSuchMessageException e) {
log.error("Cannot find the error message of internationalization, return the original error message.[{}]",request.getRequestURI());
}
return result;
}
/**
* 判断请求头的参数是否是国际版
*
* @return
*/
private boolean checkHeaderName() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
if (Objects.isNull(ra)) {
log.info("服务里RequestAttributes对象为空");
return false;
}
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getHeader(name);
if (name.equals("accept-language") && value.equals("en")) {
return true;
}
}
return false;
}
/**
* 获取请求的方法名称
*
* @param joinPoint
* @return
*/
private String getClassMethodName(JoinPoint joinPoint) {
/**
* 如果服务中所有的控制层接口名被代理,只能取uri;
*/
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (Objects.nonNull(requestAttributes)) {
HttpServletRequest request = requestAttributes.getRequest();
return request.getRequestURI();
}
//如果是子线程的话,会走下面这种情况
log.info("classMethod={}", joinPoint.getSignature().getDeclaringTypeName());
String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
log.info("className={};Method={}", className, joinPoint.getSignature().getName());
return className + "." + joinPoint.getSignature().getName();
}
}
8、VO 和控制层代码的实现:
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Size;
/**
* @author wanghuainan
* @date 2021/10/15
*/
@Data
public class UserInfo {
//@Size(min = 1, max = 10, message = "姓名长度必须为1到10")
// @NotEmpty(message = "{user.name.notBlank}")
@NotEmpty(message = "{demo.key.null}")//必须配置地址,保证不能错:basename: static/i18n/messages
//@NotEmpty()
private String name;
}
控制层:
@PostMapping("/validate1")
@ResponseBody
public ResponseDTO validate1(@Valid @Validated @RequestBody UserInfo user) throws Exception {
Map<String,Object> map = new HashMap<String, Object>();
map.put("key", "这里就是数据了");
//动态传参
String message = messageSource.getMessage("1000", new Object[]{"nandao","128"}, LocaleContextHolder.getLocale());
System.out.println("国际化后的参数"+message);
if("nandao".equals(user.getName())){
throw new Exception("抛出异常!!!");
}
return new ResponseDTO(ResponseCodeEnum.SUCCESS,map);
}
9、启动服务后依次访问接口验证:http://localhost:8080/api/validate1
多角度,正常、异常、参数非空校验、动态传参、两种切面均可以测试。
到此,参数国际化实战分享完毕,核心伪代码都贴出来了,有不明白的地方可以留言关注,积极反馈!