目录
前言
本文主要介绍转换本地化的错误消息配置文件为枚举类的设计,结合异常类来实现一致的异常管理。
开发应用程序时,一般是返回错误编码和描述来告诉客户端错误原因,而开发类库一般是通过异常类型来告诉开发员错误类型。所以,在异常处理上,开发基础类库和应用程序略有不同。基础类库一般会抛出某种具体类型的异常,而应用程序通常归纳成少量的异常类型,并在这些异常类型中包含错误编码。当然应用系统针对每个业务失败也可以编写具体的异常类,这样工作量会大点,同时也不便于统一维护。在Java中,异常可以分为如下三类
Checked Exception(受检异常):这些异常是编译器强制你去处理的,因此你必须使用try-catch语句或在方法签名中使用throws关键字来声明你不处理的异常。常见的受检异常有IOException和SQLException。
Unchecked Exception(不受检异常):也称为运行时异常,这种异常的基类是RuntimeException。这些异常不需要编译器去强制处理,它们通常是在运行时发生的错误,比如NullPointerException和IndexOutOfBoundsException。
Error(错误):错误通常表示严重的问题,如VirtualMachineError(虚拟机错误),这些问题往往不是代码能够处理的,它们往往表示虚拟机或者底层硬件出现问题。
这三种类型的基类是Throwable
问题
可以使用字符串或者数字来对错误进行 编码,为了更好的可读性,很多应用系统会使用字符串来编码,这里也采用字符串来编码。利用这些错误编码可以在前端进行国际化设计,也可以在后端进行国际化设计。不管是在前端国际化或者在后端国际化,其格式都是一个编码号和对应的错误描述。这个编码在不同的语言中都是相同的,描述则随语言而不同。
在编写应用业务处理时,会因为业务约束失败或者基础类库抛出的异常等原因而导致业务失败。返回给客户端的数据中应该包含失败的错误编码,便于提示和应用间的联调。由于错误编码在程序代码中和进行本地化的错误原因的地方都会使用,但常用的国际化配置又常用属性文件的方式。所以会出现国际化配置文件和代码中的错误编码不能对应的情况,会增加开发调试的工作量。
解决办法
这里以在Java后端做国际化为例,开发应用程序时,一般会把错误编码和描述按编码=描述的格式组织成若干个国际化文件,比如error_zh_CN.properties,error_en_US.properties的文件。在应用程序中抛出的异常中携带了错误编码,代码中的错误编码要国际化文件中错误编码相对应。为了防止拼写错误和方便以后重构,可以根据国际化文件来生成该应用系统的错误编码枚举,如果重构了properties中的编码名称,应用程序会产生编译错误。对于在前端做国际化的方式,也可以采用同样的方法来统一。
代码实现
国际化配置文件error_zh_CN.properties的内容如下
BIZ_SER_SMS_FAILED = 发送短信失败
BIZ_SER_REPEAT_PHONE = 手机号有重复
BIZ_SER_SYSTEM_BUSY = 系统很繁忙,请稍后再试
BIZ_SER_FILE_LIMIT = 文件大小超过了限制
根据错误代码配置文件转化为枚举类
package com.deamotech.iot.portal;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Test;
/**
* @author robert
* @date 2024/5/1
* @description:
*/
public class ExceptionEnumTool {
public final String PACKAGE_NAME = "com.deamotech.iot.portal.constant";
public final String ENUM_NAME = "ExceptionCodeEnum";
@Test
public void generate() throws IOException {
String projectDir = System.getProperty("user.dir");
StringBuilder pathBuilder = new StringBuilder(projectDir)
.append("/src/main/java/")
.append(PACKAGE_NAME.replace(".", "/") )
.append("/")
.append(ENUM_NAME)
.append(".java");
String filePath = pathBuilder.toString();
File file = new File(filePath);
if(file.exists()){
if(!file.delete()){
throw new IOException("can not delete old file: " + filePath);
}
}
if(!file.createNewFile()){
throw new IOException("can not create new file: " + filePath);
}
ClassLoader classLoader = ExceptionEnumTool.class.getClassLoader();
try(InputStream resourceAsStream = classLoader.getResourceAsStream("location/error_zh_CN.properties");
InputStreamReader inputStreamReader = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8);
FileWriter fileWriter = new FileWriter(filePath, StandardCharsets.UTF_8)){
Properties properties = new Properties();
properties.load(inputStreamReader);
generateEnumImpl(properties, fileWriter);
}
}
private void generateEnumImpl(Map<Object, Object> map, Writer writer) throws IOException {
// 创建一个Java源代码构建器,用于生成Enum的代码
StringBuilder codeBuilder = new StringBuilder();
codeBuilder.append("package ").append(PACKAGE_NAME).append(";\n\n");
codeBuilder.append("/**");
codeBuilder.append(" * @author robert");
codeBuilder.append(" */");
codeBuilder.append("public enum ").append(ENUM_NAME).append(" {\n");
int i = 0;
for (Map.Entry<Object, Object> entry : map.entrySet()) {
codeBuilder.append(" ").append(entry.getKey()).append("(\"").append(entry.getValue()).append("\")");
if (i < map.size() - 1) {
codeBuilder.append(",\n");
} else {
codeBuilder.append(";\n\n");
}
i++;
}
codeBuilder.append(" private final String remark;\n");
codeBuilder.append(" private ").append(ENUM_NAME).append("(String remark) {\n");
codeBuilder.append(" this.remark = remark;\n");
codeBuilder.append(" }\n\n");
codeBuilder.append(" public String getRemark() {\n");
codeBuilder.append(" return remark;\n");
codeBuilder.append(" }\n");
codeBuilder.append("}");
writer.write(codeBuilder.toString());
}
}
设计的本地化字典接口
package com.deamotech.iot.common.exception;
/**
* 异常本地化接口
*/
public interface IExceptionLocation {
String getErrorMessage(String code);
}
自定义异常的接口
package com.deamotech.iot.common.exception;
import org.apache.commons.lang3.StringUtils;
/**
* @author robert
*/
public interface IException {
String getCode();
String getCodeMessage(IExceptionLocation location);
/**
* 组装错误描述符号
* @param location
* @param code
* @param msg
* @return
*/
default String getCodeMessageImp(IExceptionLocation location, String code, String msg) {
StringBuilder sb = new StringBuilder();
if(location != null){
sb.append(location.getErrorMessage(code));
}
if(!StringUtils.isBlank(msg)){
sb.append(":");
sb.append(msg);
}
return sb.toString();
}
}
自定义的异常类
package com.deamotech.iot.common.exception;
/**
* @author robert
*/
public abstract class MyAbstractRuntimeException extends RuntimeException implements IException {
private String code;
public MyAbstractRuntimeException(Enum enum){
this(enum, "");
}
public MyAbstractRuntimeException(Enum enum, String msg){
this(enum, msg, null);
}
public MyAbstractRuntimeException(Enum enum, String msg, Exception ex){
super(msg, ex);
this.code = enum.name();
}
@Override
public String getCode(){
return code;
}
@Override
public String getCodeMessage(IExceptionLocation location) {
return getCodeMessageImp(location, code, getMessage());
}
}
应用系统在返回异常时的统一处理
package com.deamotech.iot.portal.controller;
import java.lang.reflect.Method;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.deamotech.iot.common.dto.ResultDto;
import com.deamotech.iot.common.exception.IException;
import com.deamotech.iot.common.util.JACKJSON;
import com.deamotech.iot.portal.constant.Constant;
import com.deamotech.iot.portal.service.exception.ExceptionLocationImpl;
/**
* @author robert
*/
@SuppressWarnings("rawtypes")
@RestControllerAdvice(value = { "com.deamotech.iot.portal.controller",
"com.deamotech.iot.portal.security.controller" })
public class ResponseBodyAdviseImpl implements ResponseBodyAdvice{
@Autowired
private ExceptionLocationImpl exceptionLocation;
@Override
public Object beforeBodyWrite(Object arg0,
MethodParameter arg1,
MediaType arg2,
Class arg3,
ServerHttpRequest arg4,
ServerHttpResponse arg5) {
ResultDto result = new ResultDto(true);
result.setData(arg0);
result.setTraceId(MDC.get("traceId"));
if(arg0 instanceof IException){
result.setSuccess(false);
result.setErrorCode(((IException)arg0).getCode()+"");
if(arg0 instanceof Exception){
Exception exception = (Exception)arg0;
if(StringUtils.isBlank(exception.getMessage())) {
result.setErrorMessage(((IException)arg0).getCodeMessage(exceptionLocation));
} else {
result.setErrorMessage(exception.getMessage());
}
} else {
result.setErrorMessage(((IException)arg0).getCodeMessage(exceptionLocation));
}
}else{
result.setErrorMessage("");
}
if(arg3 == StringHttpMessageConverter.class){
return JACKJSON.toJson(result);
}
// 在response头中放下请求是否成功标志,以被SysAccessStatis收集器处理。
if(arg5 instanceof ServletServerHttpResponse){
((ServletServerHttpResponse)arg5).getServletResponse()
.setHeader(Constant.RESPONSE_SUCCESS, String.valueOf(result.getSuccess()));
}
return result;
}
@Override
public boolean supports(MethodParameter methodParameter, Class arg1) {
Method m = methodParameter.getMethod();
if(m == null || m.getDeclaredAnnotation(NeedWrapper.class) != null){
return true;
}
Class cls = m.getDeclaringClass();
return cls.getDeclaredAnnotation(NeedWrapper.class) != null;
}
}
使用
在编写业务代码时,抛出异常
@ApiOperation(value = "查询当前用户基本信息")
@GetMapping(value= {"/web/sys/user/current-user","/web/current-user"})
public SysUserDto get() {
Long userId = RequestResponseUtil.getIdFromContextHolder();
if(userId == null) {
throw new RuntimeExceptionImpl(ExceptionCodeEnum.BIZ_SER_NO_LOGIN);
}
return sus.getSysUserById(userId);
}
今天是五一劳动节,希望上面的设计思路能让大家少劳动点,祝大家编码快乐!