第二十三节 SpringBoot使用AOP
一、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
二、编写扫描的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Component
public @interface LogHandler {
String packageName() default "";
String methodType() default "";
}
三、编写切面
@Aspect
@Component
public class LogAspect {
}
四、加入切点
切点就是规定【在哪里】加入通知。扫描带@LogHandler注解的方法
public class LogAspect ...
@Pointcut(value = "@annotation(com.tyzhou.aop.LogHandler)")
public void pointCutName() {
// do nothing
}
五、加入通知
@Before注解织入的是前置通知,还有后置通知与环绕通知。注解的value就是规定用扫描哪个切点。
注:如果要想让request对象在子线程中使用,要在进入线程池之前,调用一次getRequest方法,本质上是调用一次
RequestContextHolder.setRequestAttributes(requestAttributes, true);
public class LogAspect ...
@Before(value = "pointCutName()")
public void operation(JoinPoint joinPoint) {
HttpServletRequest request = getRequest();
if (request == null) {
return;
}
LOGGER.info("进入前置通知");
//被代理的对象
Object target = joinPoint.getTarget();
//方法的签名
final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//被代理的对象的方法反射
final Method methodTarget = methodSignature.getMethod();
//方法上的注解
LogHandler logHandler = methodTarget.getAnnotation(LogHandler.class);
LOGGER.error("被代理对象的包名:{}", target.getClass().getName());
LOGGER.error("被代理对象的方法名:{}", methodTarget.getName());
LOGGER.error("被代理对象的方法参数:{}", Arrays.toString(joinPoint.getArgs()));
LOGGER.error("本次请求URL:{}", request.getRequestURL());
LOGGER.error("本次请求者IP来自:{}", getIpAddress(request));
LOGGER.error("本次操作:{}", logHandler.methodType());
}
六、WebUtils与获取IP方法
public class WebUtils {
public static HttpServletRequest getRequest() {
try {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes();
//子线程共享同一个Request(在进入线程池之前,要先调用一次本方法,让程序执行到这步)
RequestContextHolder.setRequestAttributes(requestAttributes, true);
return requestAttributes.getRequest();
} catch (IllegalStateException e) {
return null;
}
}
public static HttpServletResponse getResponse() {
HttpServletResponse response;
try {
response = ((ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes()).getResponse();
} catch (Exception e) {
return null;
}
return response;
}
}
public class LogAspect ...
/**
* 获得客户端IP
*
* @param request request
* @return 客户端IP
*/
private String getIpAddress(HttpServletRequest request) {
String unknown = "unknown";
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
七、在某个Controller上加入注解并请求
多线程情况下Request对象丢失解决方案
RequestContextHodler里面有持有了一个 InheritalbeThreadLocal,InhertitableThreadLocal是能够获取到父线程的存放的线程局部变量。在WebUitls里面调用了setRequestAttributes(X,true)方法。
后记:
不要在切面中【开线程池】做任何有关于【request】对象的业务,比如记录访问者IP地址 !
如果主线程执行速度过快,那么在切面的子线程中的request对象可能会突变成null,因为主线程中的请求已经执行完毕,理所当然的释放了本次请求的request与response对象。所以,即使在切面中,我认为尽量不要开启异步线程。不使用线程池的方式,即同步得方式显得更加安全可靠。
源码下载
本章节项目源码:点我下载源代码