一、引入依赖

<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上加入注解并请求

第二十三节 SpringBoot使用AOP_spring

第二十三节 SpringBoot使用AOP_子线程_02

多线程情况下Request对象丢失解决方案

        RequestContextHodler里面有持有了一个 InheritalbeThreadLocal,InhertitableThreadLocal是能够获取到父线程的存放的线程局部变量。在WebUitls里面调用了setRequestAttributes(X,true)方法。

第二十三节 SpringBoot使用AOP_线程池_03

第二十三节 SpringBoot使用AOP_子线程_04

第二十三节 SpringBoot使用AOP_线程池_05


第二十三节 SpringBoot使用AOP_子线程_06

后记:

不要在切面中【开线程池】做任何有关于【request】对象的业务,比如记录访问者IP地址 !

        如果主线程执行速度过快,那么在切面的子线程中的request对象可能会突变成null,因为主线程中的请求已经执行完毕,理所当然的释放了本次请求的request与response对象。所以,即使在切面中,我认为尽量不要开启异步线程。不使用线程池的方式,即同步得方式显得更加安全可靠。

源码下载

        本章节项目源码:​​点我下载源代码​

    ​