大家好,我3y啊。austin
已经逐渐完善和成熟了,今天主要想跟大家聊聊austin-web
这个模块层面上的东西。
还记得在初学时,从Servlet
开始就有了web
界面,也是那时候开始,[MVC
架构]这个词就被我记住了。到后来学习使用各种的框架Strtus2
/Hibernate
/Spring
/SpringMVC
/Mybatis
/SpringBoot
都离不开dao/service/controller
这些包。
因为学习这些框架,基本都要去模拟写接口,学多了,渐渐就理解了,原来网上说的[接口]就是这么一回事啊。
后来学着发现,在接口层面上很多地方都是可以做[统一处理]的,这样一来程序设计代码结构会更优雅些。我的工作经历以来,写纯web
接口比较少,austin-web
模块的的[统一处理]基本都来源各大开发的pull request
,这个过程中我也学到了不少。
今天给大家来盘点下,看看这些设计能不能套用回你代码中!
统一接口返回值
来源:https://gitee.com/zhongfucheng/austin/pulls/2
我们的接口返回值,好是要有统一的数据结构,这样前端跟我们的交互就方便很多。这个pull request
给我提供了全局的返回类,并定义了常用的枚举类。
@Getter
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public final class BasicResultVO<T> {
/**
* 响应状态
*/
private String status;
/**
* 响应编码
*/
private String msg;
/**
* 返回数据
*/
private T data;
public BasicResultVO(String status, String msg) {
this.status = status;
this.msg = msg;
}
public BasicResultVO(RespStatusEnum status) {
this(status, null);
}
public BasicResultVO(RespStatusEnum status, T data) {
this(status, status.getMsg(), data);
}
public BasicResultVO(RespStatusEnum status, String msg, T data) {
this.status = status.getCode();
this.msg = msg;
this.data = data;
}
/**
* @return 默认成功响应
*/
public static BasicResultVO<Void> success() {
return new BasicResultVO<>(RespStatusEnum.SUCCESS);
}
/**
* 自定义信息的成功响应
* <p>通常用作插入成功等并显示具体操作通知如: return BasicResultVO.success("发送信息成功")</p>
*
* @param msg 信息
* @return 自定义信息的成功响应
*/
public static <T> BasicResultVO<T> success(String msg) {
return new BasicResultVO<>(RespStatusEnum.SUCCESS, msg, null);
}
/**
* 带数据的成功响应
*
* @param data 数据
* @return 带数据的成功响应
*/
public static <T> BasicResultVO<T> success(T data) {
return new BasicResultVO<>(RespStatusEnum.SUCCESS, data);
}
/**
* @return 默认失败响应
*/
public static <T> BasicResultVO<T> fail() {
return new BasicResultVO<>(
RespStatusEnum.FAIL,
RespStatusEnum.FAIL.getMsg(),
null
);
}
/**
* 自定义错误信息的失败响应
*
* @param msg 错误信息
* @return 自定义错误信息的失败响应
*/
public static <T> BasicResultVO<T> fail(String msg) {
return fail(RespStatusEnum.FAIL, msg);
}
/**
* 自定义状态的失败响应
*
* @param status 状态
* @return 自定义状态的失败响应
*/
public static <T> BasicResultVO<T> fail(RespStatusEnum status) {
return fail(status, status.getMsg());
}
/**
* 自定义状态和信息的失败响应
*
* @param status 状态
* @param msg 信息
* @return 自定义状态和信息的失败响应
*/
public static <T> BasicResultVO<T> fail(RespStatusEnum status, String msg) {
return new BasicResultVO<>(status, msg, null);
}
}
/**
* 全局响应状态枚举
*
* @author zzb
* @since 2021.11.17
**/
@Getter
@ToString
@AllArgsConstructor
public enum RespStatusEnum {
/**
* 错误
*/
ERROR_500("500", "服务器未知错误"),
ERROR_400("400", "错误请求"),
/**
* OK:操作成功
*/
SUCCESS("0", "操作成功"),
FAIL("-1", "操作失败"),
/**
* 客户端
*/
CLIENT_BAD_PARAMETERS("A0001", "客户端参数错误"),
TEMPLATE_NOT_FOUND("A0002", "找不到模板或模板已被删除"),
TOO_MANY_RECEIVER("A0003", "传入的接收者大于100个"),
DO_NOT_NEED_LOGIN("A0004", "非测试环境,无须登录"),
NO_LOGIN("A0005", "还未登录,请先登录"),
/**
* 系统
*/
SERVICE_ERROR("B0001", "服务执行异常"),
RESOURCE_NOT_FOUND("B0404", "资源不存在"),
/**
* pipeline
*/
CONTEXT_IS_NULL("P0001", "流程上下文为空"),
BUSINESS_CODE_IS_NULL("P0002", "业务代码为空"),
PROCESS_TEMPLATE_IS_NULL("P0003", "流程模板配置为空"),
PROCESS_LIST_IS_NULL("P0004", "业务处理器配置为空"),
;
/**
* 响应状态
*/
private final String code;
/**
* 响应编码
*/
private final String msg;
}
统一接口返回对象
在前面我们已经安排了统一接口返回BasicResultVO
,接口代码里应该都要显式地返回BasicResultVO类。但是也有办法不用显式去做,就是依赖AOP
来源:https://gitee.com/zhongfucheng/austin/pulls/45/files
当类被修饰了@AustinResult
注解,那这个类的方法都是返回BasicResultVO
对象,不需要在方法定义下显式写了。
/**
* @author kl
* @version 1.0.0
* @description 统一返回注解
* @date 2023/2/9 19:00
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AustinResult {
}
Advice拦截器:
/**
* @author kl
* @version 1.0.0
* @description 统一返回结构
* @date 2023/2/9 19:00
*/
@ControllerAdvice(basePackages = "com.java3y.austin.web.controller")
public class AustinResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private static final String RETURN_CLASS = "BasicResultVO";
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return methodParameter.getContainingClass().isAnnotationPresent(AustinResult.class) || methodParameter.hasMethodAnnotation(AustinResult.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter methodParameter, MediaType mediaType, Class aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (Objects.nonNull(data) && Objects.nonNull(data.getClass())) {
String simpleName = data.getClass().getSimpleName();
if (RETURN_CLASS.equalsIgnoreCase(simpleName)) {
return data;
}
}
return BasicResultVO.success(data);
}
}
统一入口打日志
我是很建议在接口或任务的入口打上日志,在austin
里你会发现有很多这样的实践。因为我们查问题一般就是通过日志去看程序到底发生了什么,如果入口都没有日志,那我们可能会一口咬定:你TM就没调用啊。
而austin-web
会提供各种的接口给到前端去调用嘛,这块正常也是需要打日志的。与其在各个接口入口上打日志,不如用AOP啦。
来源:https://gitee.com/zhongfucheng/austin/pulls/45/files
只要我们的Controller带有@AustinAspect
注解,就会在其方法上打印对应的日志。
/**
* @author kl
* @version 1.0.0
* @description 接口切面注解
* @date 2023/2/23 9:01
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AustinAspect {
}
切面类的逻辑也不复杂,主要就是AOP的应用:
/**
* @author kl
* @version 1.0.0
* @description 切面类
* @date 2023/2/23 9:17
*/
@Slf4j
@Aspect
@Component
public class AustinAspect {
@Autowired
private HttpServletRequest request;
/**
* 同一个请求的KEY
*/
private final String REQUEST_ID_KEY = "request_unique_id";
/**
* 只切AustinAspect注解
*/
@Pointcut("@within(com.java3y.austin.web.annotation.AustinAspect) || @annotation(com.java3y.austin.web.annotation.AustinAspect)")
public void executeService() {
}
/**
* 前置通知,方法调用前被调用
*
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
this.printRequestLog(methodSignature, joinPoint.getArgs());
}
/**
* 异常通知
*
* @param ex
*/
@AfterThrowing(value = "executeService()", throwing = "ex")
public void doAfterThrowingAdvice(Throwable ex) {
printExceptionLog(ex);
}
/**
* 打印请求日志
*
* @param methodSignature
* @param argObs
*/
public void printRequestLog(MethodSignature methodSignature, Object[] argObs) {
RequestLogDTO logVo = new RequestLogDTO();
//设置请求ID
logVo.setId(IdUtil.fastUUID());
request.setAttribute(REQUEST_ID_KEY, logVo.getId());
logVo.setUri(request.getRequestURI());
logVo.setMethod(request.getMethod());
List<Object> args = Lists.newArrayList();
//过滤掉一些不能转为json字符串的参数
Arrays.stream(argObs).forEach(e -> {
if (e instanceof MultipartFile || e instanceof HttpServletRequest
|| e instanceof HttpServletResponse || e instanceof BindingResult) {
return;
}
args.add(e);
});
logVo.setArgs(args.toArray());
logVo.setProduct("austin");
logVo.setPath(methodSignature.getDeclaringTypeName() + "." + methodSignature.getMethod().getName());
logVo.setReferer(request.getHeader("referer"));
logVo.setRemoteAddr(request.getRemoteAddr());
logVo.setUserAgent(request.getHeader("user-agent"));
log.info(JSON.toJSONString(logVo));
}
/**
* 打印异常日志
*
* @param ex
*/
public void printExceptionLog(Throwable ex) {
JSONObject logVo = new JSONObject();
logVo.put("id", request.getAttribute(REQUEST_ID_KEY));
log.error(JSON.toJSONString(logVo), ex);
}
}
统一枚举字段
在austin
的开发过程中,我定义了很多枚举。虽然不是特意的,但大多数枚举我的属性都是code
+description
。一般要在枚举类的代码里写GetEnumByCode()
这样的方法
在前段时间接收了个pull request
,写了个公共的枚举接口,然后做了个工具类去统一掉常见的枚举方法,这样就不用在枚举下写这种重复的方法了。
来源:https://github.com/ZhongFuCheng3y/austin/pull/33
/**
* @author kyw7
* 枚举接口
*/
interface PowerfulEnum {
/**
* 获取枚举的code
*
* @return
*/
Integer getCode();
/**
* 获取枚举的描述
*
* @return
*/
String getDescription();
}
枚举工具类:
/**
* @author kyw7
* 枚举工具类(获取枚举的描述、获取枚举的code、获取枚举的code列表)
*/
public class EnumUtil {
private EnumUtil() {
}
public static <T extends PowerfulEnum> String getDescriptionByCode(Integer code, Class<T> enumClass) {
return Arrays.stream(enumClass.getEnumConstants())
.filter(e -> Objects.equals(e.getCode(), code))
.findFirst().map(PowerfulEnum::getDescription).orElse("");
}
public static <T extends PowerfulEnum> T getEnumByCode(Integer code, Class<T> enumClass) {
return Arrays.stream(enumClass.getEnumConstants())
.filter(e -> Objects.equals(e.getCode(), code))
.findFirst().orElse(null);
}
public static <T extends PowerfulEnum> List<Integer> getCodeList(Class<T> enumClass) {
return Arrays.stream(enumClass.getEnumConstants())
.map(PowerfulEnum::getCode)
.collect(Collectors.toList());
}
}
统一异常拦截
我自己写代码的风格,我是不愿意一直往外抛异常的。很多时候,就是得需要try catch
不让中断请求的完成流程的。(比如我用redis
来做缓存,如果redis
挂了,不应该中断整个请求,把日志打下来,告警就可以了。)
我喜欢将异常都在控制在自己的手里(自己写try catch
,自己中断流程),我不是[异常统一拦截]的拥簇者,但我不反对。
来源:https://gitee.com/zhongfucheng/austin/pulls/45/files
/**
* @author kl
* @version 1.0.0
* @description 拦截异常统一返回
* @date 2023/2/9 19:03
*/
@ControllerAdvice(basePackages = "com.java3y.austin.web.controller")
@ResponseBody
public class ExceptionHandlerAdvice {
private static final Logger log = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);
public ExceptionHandlerAdvice() {
}
@ExceptionHandler({Exception.class})
@ResponseStatus(HttpStatus.OK)
public BasicResultVO exceptionResponse(Exception e) {
BasicResultVO result = BasicResultVO.fail(RespStatusEnum.ERROR_500, "\r\n" + Throwables.getStackTrace(e) + "\r\n");
log.error(Throwables.getStackTrace(e));
return result;
}
@ExceptionHandler({CommonException.class})
@ResponseStatus(HttpStatus.OK)
public BasicResultVO commonResponse(CommonException ce) {
log.error(Throwables.getStackTrace(ce));
return new BasicResultVO(ce.getCode(), ce.getMessage(), ce.getRespStatusEnum());
}
}