应对场景
在一些项目中,会有这样的一个场景,对某个接口所接受到的所有请求,进行入库记录,并记载该次请求的状态(成功与否),或业务中涉及任务时,也需要对该任务进行入库记录甚至跟踪。
最简单的实现方式,是在controler层接收到请求后,解析入参并入库保存请求(任务)信息,service层处理完逻辑后再解析返回并更新任务信息。
但这只是一次顺利的请求并成功返回的场景,如果接口返回失败,或在service层的任意地方发生异常、err,都让任务的数据库更新操作很难完成。当然,应对代码抛出异常的问题,结合全局参数异常捕获器(@RestControllerAdvice)也能有效解决任务更新问题,但代码总归是比较臃肿。
解决方案
使用AOP编程思想,结合反射、注解等知识,为接口搭建“门卫”,达到出入记录的目的。
详细逻辑是,在controller层接收到请求前,对请求进行解析并入库记录,在controller层接口返回后(包括成功,失败,异常)对请求进行解析并入库更新。
思路整理:
- 在接口方法上加上自定义注解,以接口方法为接入点处理逻辑
- @Before()在切点前解析并记录请求信息
- @AfterReturning()在切点返回内容后解析并更新请求信息
具体实现
自定义注解Warehousing.java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Warehousing {
String description() default "";
}
入库切片类WarehousingAspect.java,实现上注解的具体动作。
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Component
@Aspect
@Slf4j
public class WarehousingAspect {
@Autowired
private WarehousingService warehousingService;
@Pointcut("@annotation(com.asiainfo.upc.northward.aspect.Warehousing)")
public void warehousing() {}
@Around("warehousing()")
public Object doAroud(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
return result;
}
@Before("warehousing()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("===============================request start===============================");
log.info("URL : {}",request.getRequestURL().toString());
log.info("HTTP Method : {}", request.getMethod());
log.info("Class Method : {},{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
log.info("IP : {}", request.getRemoteAddr());
Object[] args = joinPoint.getArgs();
log.info("Request Args : {}", JSONObject.toJSONString(args));
User user = joinPoint.getArgs()[0];
startInsert(user);
}
@AfterReturning(value = "warehousing()",returning = "rvt")
public void doAfterReturning(JoinPoint joinPoint, Object rvt) throws Throwable {
User user = joinPoint.getArgs()[0];
Result rvt = (Result)rvt;
endInsert(user, rvt);
log.info("Response Args : {}", JSONObject.toJSONString(rvt));
log.info("===============================request end===============================");
}
private void startInsert(User user) {
}
private void endInsert(User user,Result rvt) {
}
}
入参类User.java
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Data
public class User {
private String string;
}
响应类Result.class
import lombok.Data;
@Data
public class Result<T> implements Serializable {
private String msg = "success";
private int code = 200;
private T data;
public Result() {
}
public Result(T data) {
this.data = data;
}
public Result(Throwable e) {
this.msg = e.toString();
this.code = 500;
}
}
注解的使用示例Controller.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*
import javax.validation.Valid;
@Slf4j
@RestController
public class Controller {
@PostMapping("/updataUser")
@Warehousing(description = "任务入库")
public Result<String> updataUser(User user) {
}
}
总结
- 这种模式下的请求入库记录、日志打印等操作,代码简洁,逻辑简单,且具备很强的可迁移性。
- 这样的设计模式,还为接口的其他处理预留了很多可操作空间。
- 本文为了能更直观、简洁的说明过程,入库逻辑写死了入参类型与出参类型,注解@Warehousing()使用范围有局限性,有兴趣或有需求的兄弟,可以对入库方法startInsert()等进行重写,使用反射技术、策略模式、模板方法等,实现@Warehousing()的通用性,对所有接口的入参出参进行记录并入库。(这块儿逻辑甚至可以考虑写在网关里,但一版项目不会有记录入库所有http请求的业务需求)。
|