【Spring AOP】利用spring aop实现自定义注解功能

本文讲解spring aop的体系结构并利用spring aop实现自定义注解功能。

一、何为AOP

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。下图就是AOP的核心概念和学习路线图,掌握此图是关键

在这里插入图片描述

二、需求举例

比如我们开发中对某个页面进行访问限制,需要在某个方法前和方法后都执行一段代码,用来验证权限等,怎么实现?

1、写死代码
public interface Log {
    void log(String info);
}
public class LogImpl implements Log { 
    @Override
    public void log(String info) {
        before();
        System.out.println("info: " + info);
        after();
    }
    private void before() {
        System.out.println("Before");
    }
    private void after() {
        System.out.println("After");
    }
}

比如我们要统计每个方法的执行时间,以对性能作出评估,那是不是要在每个方法的一头一尾都做点手脚呢?这样写死的方法会累死码农们,于是来一个加强版。

2、静态代理

单独为 LogImpl 这个类写一个代理类,接口还是未变,实现类抽取出来放一边,这样就进行了解耦,后置和前置功能的实现放到这个静态代理类中去绑定结合。

//Log接口的实现类
public class LogImpl implements Log { 
    @Override
    public void log(String info) {
        System.out.println("info: " + info);
    }
}

//绑定前置和后置方法的静态代理类
public class LogProxy implements Log {
    private LogImpl logImpl;
    public LogProxy(LogImpl logImpl) {
        this.logImpl = logImpl;
    }
    @Override
    public void log(String info) {
        before();
        logImpl.log(info);
        after();
    }
    private void before() {
        System.out.println("Before");
    }
    private void after() {
        System.out.println("After");
    }
}
//用这个 LogProxy 去代理 LogImpl,下面看看客户端如何来调用:
public class Client {
    public static void main(String[] args) {
        LogProxy logProxy = new LogProxy(new LogImpl());
        logProxy.log("老板,我的工作做完了");
    }
}
3、JDK动态代理

所有的代理类都合并到动态代理类中了,该代理类设置为泛型,可以接收各种实现类,经过代理类生成后可以绑定结合前置、后置方法,进行功能增强。


public class JDKDynamicProxy implements InvocationHandler {
 
    private Object target;
 
    public JDKDynamicProxy(Object target) {
        this.target = target;
    }
 
    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }
 
    private void before() {
        System.out.println("Before");
    }
 
    private void after() {
        System.out.println("After");
    }
}
//客户端调用
public class Client {
    public static void main(String[] args) {
       JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(new LogImpl()).getProxy();
       jdkDynamicProxy.log("报告老板");
    }
}

所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK 给我们提供的动态代理只能代理接口,而不能代理没有接口的类。

4、cglib动态代理

我们使用开源的 CGLib 类库可以代理没有接口的类,这样就弥补了 JDK 的不足。

public class CGLibDynamicProxy implements MethodInterceptor {
 
    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
 
    private CGLibDynamicProxy() {
    }
 
    public static CGLibDynamicProxy getInstance() {
        return instance;
    }
 
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, this);
    }
 
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = proxy.invokeSuper(target, args);
        after();
        return result;
    }
 
    private void before() {
        System.out.println("Before");
    }
 
    private void after() {
        System.out.println("After");
    }
}
//
public class Client {
    public static void main(String[] args) {
        Log log = CGLibDynamicProxy.getInstance().getProxy(LogImpl.class);
        log.log("Jack");
    }
}

三、AOP概念

1、切面(Aspect)

其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。

2、增强(Advice)

是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。

3、 连接点(Joinpoint)

程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但spring只支持方法级的连接点。

4、切入点(Pointcut)

用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

5、目标对象(Target)

就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。

6、代理对象(Proxy)

将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。

7、织入(Weaving)

将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。

8、引入(Introduction)

用以区分类和方法的拦截。

四、AOP编程实现

实现AOP有很多种方式,我们采用Spring + AspectJ的方式,需要引入pom.xml依赖

         <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <!-- 利用注解切面编程aspectj依赖 -->
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
1、基于注解:通过 AspectJ execution 表达式拦截方法

通过表达式去匹配各个符合条件的切入点,如下就实现了com.ratel.test.service包下的任意方法拦截匹配,那么匹配后的切入点都会织入代理中。

/通过表达式匹配方法
@Aspect
@Component
public class TimeConsumeAspect {
    // 第一个*代表返回类型不限
    // 第二个*代表所有类
    // 第三个*代表所有方法
    // (..) 代表参数不限
    @Pointcut("execution(public * com.ratel.test.service.*.*(..))")
    @Order(2)
    public void pointCut(){};

    @Before(value = "pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }

    private void before() {
        System.out.println("Before");
    }

    private void after() {
        System.out.println("After");
    }

虽然有了execution拦截表达式,但是有时候我们只想满足精确的某些方法,靠表达式难免会有缺漏或者是带入了一些不想带入的切入点进来,这可是很考验写拦截表达式的人,我们可以采用点对点的更精确的基于注解的方法。

2、基于注解:通过 AspectJ @annotation 表达式拦截方法

首先,定义注解ErrorLog类

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Documented
public @interface ErrorLog { String value() default ""; }

再次,定义切面类处理

@Aspect
@Component
public class ErrorLogAspect {

    @Pointcut("@annotation(ErrorLog)")
    public void annotationPointcut() {
    }

    /**
     * 前置通知
     * @param joinPoint
     */
    @Before("annotationPointcut()")
    public void beforePointcut(JoinPoint joinPoint) {
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ErrorLog annotation = method.getAnnotation(ErrorLog.class);
        String value = annotation.value();
        System.out.println("beforePointcut:"+value);
    }

    /**
     * 后置通知
     * @param joinPoint
     */
    @After("annotationPointcut()")
    public void afterPointcut(JoinPoint joinPoint) {
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ErrorLog annotation = method.getAnnotation(ErrorLog.class);
        String value = annotation.value();
        System.out.println("afterPointcut:"+value);
    }

    /**
     * aorund 环绕通知
     * @param joinPoint
     */
    @Around("annotationPointcut()")
    public void aroundPointCut(JoinPoint joinPoint){
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ErrorLog annotation = method.getAnnotation(ErrorLog.class);
        String value = annotation.value();
        System.out.println("aroundPointcut:"+value);
    }

    @AfterReturning("annotationPointcut()")
    public void afterReturning(JoinPoint joinPoint){
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ErrorLog annotation = method.getAnnotation(ErrorLog.class);
        String value = annotation.value();
        System.out.println("afterReturning:"+value);
    }

    @AfterThrowing("annotationPointcut()")
    public void afterThrowing(JoinPoint joinPoint){
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ErrorLog annotation = method.getAnnotation(ErrorLog.class);
        String value = annotation.value();
        System.out.println("发生异常:"+value);
    }
}

注意:

当方法符合切点规则不符合环绕通知的规则时候,执行的顺序如下

@Before→@After→@AfterRunning(如果有异常→@AfterThrowing)

当方法符合切点规则并且符合环绕通知的规则时候,执行的顺序如下

@Around→@Before→@After→@Around执行 ProceedingJoinPoint.proceed() 之后的操作→@AfterRunning(如果有异常→@AfterThrowing)

返回通知和异常通知不会同时出现;不管是否发生异常,后置通知都会正常打印。

内容来源:https://www.jianshu.com/p/a82509c4bb0d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值