spring-aop Aspect Oriented Programming 面向切面编程
什么是AOP? 是面向切面的一种思想
理解:如果你写了5个方法每个方法里都需要对某些变量进行检查,并且保存一种格式的日志。
这个时候你怎么做?
每个方法里都写一遍?
那样可以实现 是抽象成接口写一个代理类
Spring提供了一种简单的方式就是切面
每个动作就是一个切面。把这个面通过一种匹配的规则匹配到你想要加到的代码的任意位置
在java平台有3种切入方式
编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,
AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入(动态代理)
Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口
如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现
AOP的术语
Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
Pointcut:切入点,即一组连接点的集合;
Advice:增强,指特定连接点上执行的动作;
Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
Weaving:织入,指将切面整合到程序的执行流程中;
Interceptor:拦截器,是一种实现增强的方式;
Target Object:目标对象,即真正执行业务的核心逻辑对象;
AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
实例使用中理解上面的术语
spring-aop使用第一步
1. maven引入坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
上述依赖会自动引入AspectJ,使用AspectJ实现AOP比较方便,因为它的定义比较简单
2. 编写Aspect
@Aspect
@Component
public class LoggingAspect {
// 在执行UserService的每个方法前执行:
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
执行UserService的每个public方法前执行doAccessCheck()代码
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
// 在执行MailService的每个方法前后执行:
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
@Around可以决定是否执行目标方法,因此,我们在doLogging()内部先打印日志,
再调用方法,最后打印日志后返回结果
@Around根据名字就可以知道是环绕
所以在方法执行前就会进来这个函数
然后可以做些业务的逻辑判断 ,是否执行这个方法 如果执行就写Object retVal = pjp.proceed();
在这个执行方法后还可以继续写逻辑的
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
System.err.println("[Around] start " + pjp.getSignature());
Object retVal = pjp.proceed();
System.err.println("[Around] done " + pjp.getSignature());
return retVal;
}
}
3. 第三部就得给spring项目的配置类上加上启用aspect的注解。就会去执行上边的注解
@Configuration
@ComponentScan
@EnableAspectJAutoProxy // 加上这个注解
public class AppConfig {
...
}
以上三步就写成了一个aop。关键点是。别人的业务代码你都没动,那人可能都不知道你加了这些。是在做一些定制任务时,会很常用的
php里也支持这个aop。就是那个call function .通过自己写逻辑。可以通过读取配置文件连实现hook
原理:
Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类。
可见,虽然Spring容器内部实现AOP的逻辑比较复杂(需要使用AspectJ解析注解,并通过CGLIB实现代理类),但我们使用AOP非常简单,一共需要三步:
定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法;
标记@Component和@Aspect;
在@Configuration类上标注@EnableAspectJAutoProxy。
至于AspectJ的注入语法则比较复杂,请参考Spring文档
拦截器的类型
@Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
@After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
@AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
@AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
@Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能
使用注解来精准匹配
上面例子中的 execution(public * com.itranswarp.learnjava.service.UserService.*(..))
这个是有弊端的。可能不是特别精准的匹配到
实际的工作场景中 也是我写了一个aspect类。你在你类里边用一下
比如 数据库的事物就是用了一个@Transactional
这样精准,随用谁就自己加个注解就行
这时候我们写切面就得写个注解了来实现切入了
举例:可以找找好多开源的java框架代码都是这么写的
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value();
}
在需要被监控的关键方法上标注该注解:
@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}
然后,我们定义MetricAspect:
@Aspect
@Component
public class MetricAspect {
@Around("@annotation(metricTime)")
他会找这个注解所在位置。只要有这个注解了就会切进去. 是不是很好 那个excution也不用写了
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value();
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}
注意metric()方法标注了@Around("@annotation(metricTime)"),
它的意思是,符合条件的目标方法是带有@MetricTime注解的方法,
因为metric()方法参数类型是MetricTime(注意参数名是metricTime不是MetricTime),
我们通过它获取性能监控的名称。
有了@MetricTime注解,再配合MetricAspect,任何Bean,只要方法标注了@MetricTime注解,就可以自动实现性能监控
AOP避坑指南
结论:
访问被注入的Bean时,总是调用方法而非直接访问字段; 用get set 访问字段 我相应这是共识了,应该没多少人会错
编写Bean时,如果可能会被代理,就不要编写public final方法。 方法上不写final 因为spring基于的是方法
这样才能保证有没有AOP,代码都能正常工作
原因:
没必要初始化proxy的成员变量,因为proxy的目的是代理方法
对于Spring通过CGLIB动态创建的UserService$$EnhancerBySpringCGLIB代理类,
它的构造方法中,并未调用super(),因此,从父类继承的成员变量,包括final类型的成员变量,统统都没有初始化。
这是因为自动加super()的功能是Java编译器实现的,它发现你没加,就自动给加上,发现你加错了,就报编译错误。
但实际上,如果直接构造字节码,一个类的构造方法中,不一定非要调用super()。
Spring使用CGLIB构造的Proxy类,是直接生成字节码,并没有源码-编译-字节码这个步骤,因此:
Spring通过CGLIB创建的代理类,不会初始化代理类自身继承的任何成员变量,包括final类型的成员变量!
为什么Spring刻意不初始化Proxy继承的字段
因为spring没法判断你这个自动有没有进行过其他的操作。可能你在其他的方法里变成别的值了都是可能的 所以代理类没法初始化