aop

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没法判断你这个自动有没有进行过其他的操作。可能你在其他的方法里变成别的值了都是可能的 所以代理类没法初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰明子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值