3、AOP详解
3.1、Aop常用术语
1.连接点(Join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。类中的哪些方法可以被增强,这些方法就被称作为连接点。
2.切点(PointCut): 可以插入增强处理的连接点,实际被增强的方法就称作为切入点
3.通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理, 实际增强的业务逻辑,该过程就可以称作为通知 前置、后置、环绕通知
4.切面(Aspect): 切面是通知和切点的结合。 把通知应用到的过程 就是为切面
5.引入(Introduction):允许我们向现有的类添加新的方法或者属性。
6.织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的代理对象
1.连接点 该类中哪些方法需要被增强,这些方法就可以称作连接点
3.切点 实际被增强的方法
2.通知 在方法前后执行代码
前置通知 调用方法之前处理...
后置通知 调用完该方法之后处理
环绕通知 在我们被代理方法前后执行
异常通知
最终通知
4.切面 把通知应用到的过程 就是为切面
3.2、Aop环境准备
1.Spring框架一般都是基于AspectJ实现AOP操作
(1)什么是AspectJ
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,AspectJ不是Spring组成部分,独立AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作.
2.基于AspectJ实现AOP
(1)基于xml配置文件实现
(2)基于注解方式(偏多的)
3.在项目工程目录引入AOP依赖
3.2.1、maven依赖
<!-- aspectj支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.apache.geronimo.bundles</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8_2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
3.2.2、切入点表达式
具体那个类中的那个方法来实现增强
需要描述 该类中哪些方法是需要被增强-----切入点规则
execution( [权限修饰符] [返回类型] [类全路径] [方法名称] ([参数列表]));
[权限修饰符
1.public.String.com.mayikt.service.MayiktService.addUser(..) --拦截的是
MayiktService类中addUser方法名称 所有参数 返回值String
2.* com.mayikt.service.MayiktService.*(..)拦截我们的
MayiktService类中的所有方法
3.* com.mayikt.service.*.*(..)拦截就是我们 com.mayikt.service.包下
的所有的类所有的方法。
(1)语法接口:
execution( [权限修饰符] [返回类型] [类全路径] [方法名称] ([参数列表]));
//举例1:对com.mayikt.service.MayiktService类里面的 add() 进行增强 execution(*com.mayikt.service.MayiktService.add(..)); // * 表示所有, .. 表示参数列表
//举例2:对com.mayikt.service.MayiktService类里面的 所有方法 进行增强 execution(*com.mayikt.service.MayiktService.*(..));
//举例3:对com.mayikt.service.MayiktService所有类里面的 所有方法 进行增强 execution(*com.mayikt.service.MayiktService.*.*(..));
3.2.3、切入点表达式测试代码
package com.mayikt.service;
import org.springframework.stereotype.Component;
@Component
public class MayiktService {
public String addMayikt() {
System.out.println("addMayikt...");
return "ok";
}
}
package com.mayikt.proxy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect// aop 代理
public class UserProxy {
/**
* 前置通知
*/
@Before("execution(* com.mayikt.service.MayiktService.addMayikt(..));")
public void before() {
System.out.println("前置通知...");
}
/**
* 后置通知
*/
@After("execution(* com.mayikt.service.MayiktService.addMayikt(..));")
public void after() {
System.out.println("后通知...");
}
/**
* 环绕通知
*/
@Around(value = "execution(* com.mayikt.service.MayiktService.addMayikt(..));")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知...");
System.out.println("目标方法之前开始执行...");
Object result = proceedingJoinPoint.proceed();
System.out.println("目标方法之后开始执行...");
return result;
}
//@AfterReturning表达后置通知/返回通知,表达方法返回结果之后执行
@AfterReturning(value = "execution(* com.mayikt.service.MayiktService.addMayikt(..));")
public void afterReturning() {
System.out.println("afterReturning");
}
//@AfterThrowing表达异常通知
@AfterThrowing(value = "execution(* com.mayikt.service.MayiktService.addMayikt(..));")
public void afterThrowing() {
System.out.println("afterThrowing");
}
}
3.2.4、开启springaop
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<!--开启注解方式 -->
<context:component-scan base-package="com.mayikt"></context:component-scan>
<!--开启 aspectj 生成代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
ClassPathXmlApplicationContext app =
new ClassPathXmlApplicationContext("spring_07.xml");
MayiktService mayiktService = app.getBean("mayiktService", MayiktService.class);
mayiktService.addMayikt();
3.3、spring框架种使用 cglib?jdk动态代理?
spring aop 底层基于 代理封装?
如果我们 被代理类 没有实现接口的情况下 则使用 cglib动态代理
如果我们被代理类 有实现接口的情况下 则使用 jdk动态代理
3.4、Aop实现统一日志输出
3.4.1、注解定义
package com.cjs.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SystemControllerLog {
String description() default "";
boolean async() default false;
}
package com.cjs.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SystemServiceLog {
String description() default "";
boolean async() default false;
}
3.4.2、定义一个类包含所有需要输出的字段
package com.cjs.example.service;
import lombok.Data;
import java.io.Serializable;
@Data
public class SystemLogStrategy implements Serializable {
private boolean async;
private String threadId;
private String location;
private String description;
private String className;
private String methodName;
private String arguments;
private String result;
private Long elapsedTime;
public String format() {
return "线程ID: {}, 注解位置: {}, 方法描述: {}, 目标类名: {}, 目标方法: {}, 调用参数: {}, 返回结果: {}, 花费时间: {}";
}
public Object[] args() {
return new Object[]{this.threadId, this.location, this.description, this.className, this.methodName, this.arguments, this.result, this.elapsedTime};
}
}
3.4.3、定义切面
package com.cjs.example.aspect;
import com.alibaba.fastjson.JSON;
import com.cjs.example.annotation.SystemControllerLog;
import com.cjs.example.annotation.SystemRpcLog;
import com.cjs.example.annotation.SystemServiceLog;
import com.cjs.example.enums.AnnotationTypeEnum;
import com.cjs.example.service.SystemLogStrategy;
import com.cjs.example.util.JsonUtil;
import com.cjs.example.util.ThreadUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
@Aspect
public class SystemLogAspect {
private static final Logger LOG = LoggerFactory.getLogger(SystemLogAspect.class);
private static final Logger LOG = LoggerFactory.getLogger(SystemLogAspect.class);
@Pointcut("execution(* com.ourhours..*(..)) && !execution(* com.ourhours.logging..*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object doInvoke(ProceedingJoinPoint pjp) {
long start = System.currentTimeMillis();
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
LOG.error(throwable.getMessage(), throwable);
throw new RuntimeException(throwable);
} finally {
long end = System.currentTimeMillis();
long elapsedTime = end - start;
printLog(pjp, result, elapsedTime);
}
return result;
}
/**
* 打印日志
* @param pjp 连接点
* @param result 方法调用返回结果
* @param elapsedTime 方法调用花费时间
*/
private void printLog(ProceedingJoinPoint pjp, Object result, long elapsedTime) {
SystemLogStrategy strategy = getFocus(pjp);
if (null != strategy) {
strategy.setThreadId(ThreadUtil.getThreadId());
strategy.setResult(JsonUtil.toJSONString(result));
strategy.setElapsedTime(elapsedTime);
if (strategy.isAsync()) {
new Thread(()->LOG.info(strategy.format(), strategy.args())).start();
}else {
LOG.info(strategy.format(), strategy.args());
}
}
}
/**
* 获取注解
*/
private SystemLogStrategy getFocus(ProceedingJoinPoint pjp) {
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
Object[] args = pjp.getArgs();
String targetClassName = pjp.getTarget().getClass().getName();
try {
Class<?> clazz = Class.forName(targetClassName);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (methodName.equals(method.getName())) {
if (args.length == method.getParameterCount()) {
SystemLogStrategy strategy = new SystemLogStrategy();
strategy.setClassName(className);
strategy.setMethodName(methodName);
SystemControllerLog systemControllerLog = method.getAnnotation(SystemControllerLog.class);
if (null != systemControllerLog) {
strategy.setArguments(JsonUtil.toJSONString(args));
strategy.setDescription(systemControllerLog.description());
strategy.setAsync(systemControllerLog.async());
strategy.setLocation(AnnotationTypeEnum.CONTROLLER.getName());
return strategy;
}
SystemServiceLog systemServiceLog = method.getAnnotation(SystemServiceLog.class);
if (null != systemServiceLog) {
strategy.setArguments(JsonUtil.toJSONString(args));
strategy.setDescription(systemServiceLog.description());
strategy.setAsync(systemServiceLog.async());
strategy.setLocation(AnnotationTypeEnum.SERVICE.getName());
return strategy;
}
return null;
}
}
}
} catch (ClassNotFoundException e) {
LOG.error(e.getMessage(), e);
}
return null;
}
}
3.4.4、配置
这里也可以用组件扫描,执行在Aspect上加@Component注解即可,但是这样的话有个问题。
就是,如果你的这个Aspect所在包不是Spring Boot启动类所在的包或者子包下就需要指定@ComponentScan,因为Spring Boot默认只扫描和启动类同一级或者下一级包。
package com.cjs.example.config;
import com.cjs.example.aspect.SystemLogAspect;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@AutoConfigureOrder(2147483647)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnClass(SystemLogAspect.class)
@ConditionalOnMissingBean(SystemLogAspect.class)
public class SystemLogAutoConfiguration {
@Bean
public SystemLogAspect systemLogAspect() {
return new SystemLogAspect();
}
}
3.4.5、自动配置(resources/META-INF/spring.factories)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ourhours.logging.config.SystemLogAutoConfiguration
3.4.6、其它工具类
3.4.6.1. 获取客户端IP
package com.cjs.example.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return servletRequestAttributes.getRequest();
}
public static String getIpAddress() {
HttpServletRequest request = getHttpServletRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
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.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
}else if (ip != null && ip.length() > 15) {
String[] ips = ip.split(",");
for (int index = 0; index < ips.length; index++) {
String strIp = (String) ips[index];
if (!("unknown".equalsIgnoreCase(strIp))) {
ip = strIp;
break;
}
}
}
return ip;
}
}
3.4.6.2. 格式化成JSON字符串
package com.cjs.example.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class JsonUtil {
public static String toJSONString(Object object) {
return JSON.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect);
}
}
3.4.6.3. 存取线程ID
package com.cjs.example.util;
import java.util.UUID;
public class ThreadUtil {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static String getThreadId() {
String threadId = threadLocal.get();
if (null == threadId) {
threadId = UUID.randomUUID().toString();
threadLocal.set(threadId);
}
return threadId;
}
}
3.4.7、同时还提供静态方法
package com.cjs.example;
import com.cjs.example.util.JsonUtil;
import com.cjs.example.util.ThreadUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Log {
private static Logger LOGGER = null;
private static class SingletonHolder{
public static Log instance = new Log();
}
private Log(){}
public static Log getInstance(Class<?> clazz){
LOGGER = LoggerFactory.getLogger(clazz);
return SingletonHolder.instance;
}
public void info(String description, Object args, Object result) {
LOGGER.info("线程ID: {}, 方法描述: {}, 调用参数: {}, 返回结果: {}", ThreadUtil.getThreadId(), description, JsonUtil.toJSONString(args), JsonUtil.toJSONString(result));
}
public void error(String description, Object args, Object result, Throwable t) {
LOGGER.error("线程ID: {}, 方法描述: {}, 调用参数: {}, 返回结果: {}", ThreadUtil.getThreadId(), description, JsonUtil.toJSONString(args), JsonUtil.toJSONString(result), t);
}
}
3.4.8、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-logging</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cjs-logging</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<aspectj.version>1.8.13</aspectj.version>
<servlet.version>4.0.0</servlet.version>
<slf4j.version>1.7.25</slf4j.version>
<fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>