面向切面编程(Aspect Oriented Programming) AOP :通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP类似一种装饰器,它可以在不修改源代码的情况下拓展其他的功能;这样既达成了需求,也不会违反OOP原则

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。

    • 切面也就是一个个的逻辑代码模块:例如日志模块、权限模块等

  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。

    • 连接点是程序执行过程中明确的点,一般是类中方法的调用。连接点是程序在运行过程中能够插入切面的地点,比如方法调用、异常抛出、字段修改等。

  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

    • 切点就是需要在哪个地方切入 也就是讲你逻辑代码放置的地方 在 Advice(通知)中定义

  • Advice(通知):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。

    • 通知可以分为前置通知Before、后置通知AfterReturning、异常通知AfterThrowing、最终通知After、环绕通知Around五类。

  • Target(目标对象):织入 Advice 的目标对象.。

    • 需要将新逻辑代码(方法)合并到其他代码(方法)中的对象

  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

    • 通过切点合并在一起 将创建的切面 应用到目标的对象从而创建新的代理对象的过程 ;通过动态代理和反射来实现

AOP可以在不影响我们业务的情况下 实现动态的增强

2、 AOP的实现

假设在 CRUD 功能的基础上需要加上日志的功能

这个时候就需要用到AOP来实现

首先要实现AOP必须要导入对应的包:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.8</version>
</dependency>

然后 applicationContext.xml 中添加aop的约束

<?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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
​
</beans>

2.1 配置文件实现AOP

配置文件实现AOP有两种方式:

2.1.1 方式一:使用Spring API接口来实现

UserService:

package cn.mianju.demo01.service;
​
public interface UserService {
    void add();
    void delect();
    void modify();
    void query();
}

UserServiceImpl:

package cn.mianju.demo01.service;
​
public class UserServiceImpl implements UserService{
​
    @Override
    public void add() {
        System.out.println("add");
    }
​
    @Override
    public void delect() {
        System.out.println("delect");
    }
​
    @Override
    public void modify() {
        System.out.println("modify");
    }
​
    @Override
    public void query() {
        System.out.println("query");
    }
}

然后现在 需要写两个日志类,一个在模块运行前 一个模块运行后

Log(运行前日志):

package cn.mianju.demo01.log;
​
import org.springframework.aop.MethodBeforeAdvice;
​
import java.lang.reflect.Method;
​
public class Log implements MethodBeforeAdvice {
​
​
    /**
     * @param method :要执行的目标对象的方法
     * @param args :参数
     * @param target :目标对象 就是真实对象的那个类
     * **/
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("方法是" + method + " | " + "target是" + target);
    }
}
​

解析:

  • method:是代理后类中将要执行的方法

  • args:传入的参数

  • target:是代理的真是类/对象

注意:这个方法会在运行前运行一遍,需要实现MethodBeforeAdvice接口 并重写before方法

before 中的代码就是会在模块运行前运行一遍的代码

AfterLog(运行后日志):

package cn.mianju.demo01.log;
​
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
​
import java.lang.reflect.Method;
​
public class Afterlog implements AfterReturningAdvice {
​
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("这是返回之后的方法:Afterlog");
    }
}

解析:

  • returnValue:模块执行完后的返回值

  • method:方法

  • args:参数

  • target:代理的真实对象

注意:这个方法会在运行前运行一遍,需要实现AfterReturningAdvice接口 并重写afterReturning方法

afterReturning 中的代码就是会在模块运行前运行一遍的代码

然后到 applicaitonContext.xml 中去进行aop的配置

<?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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
​
<!--    注册bean-->
    <bean id="userService" class="cn.mianju.demo01.service.UserServiceImpl"/>
    <bean id="log" class="cn.mianju.demo01.log.Log"/>
    <bean id="Afterlog" class="cn.mianju.demo01.log.Afterlog"/>
​
<!--    配置aop:需要导入aop的约束-->
    <aop:config>
<!--        切入点:expression表达式:execution(*:返回值类型 cn.mianju…….*(..):某个类下所有的方法包含所有参数的)-->
        <aop:pointcut id="userService1" expression="execution(* cn.mianju.demo01.service.UserServiceImpl.*(..))"/>
​
<!--        执行环绕增强-->
        <aop:advisor advice-ref="log" pointcut-ref="userService1"/>
        <aop:advisor advice-ref="Afterlog" pointcut-ref="userService1"/>
    </aop:config>
​
</beans>

解析:

  • 首先标签 <aop:config>是配置aop的标签,在标签中进行aop的设置

  • aop设置需要一个切入点,使用 <aop:pointcut> 来表示 其中有个属性是 expression 是个表达式,表达式中的内容就是切入的点

    • expression(返回值类型 返回值的类+方法+参数)

    • 如果是所有的方法和参数就是 (* cn.mianju.demo01.service.UserServiceImpl.*(..)) 两个 .. 很重要 代表所有的参数

  • 然后就执行环绕增强 也就是执行两个log方法

    • 参数的 advice-ref 就是上面注册的log的bean

    • 参数的 pointcut-ref 就是 <aop:pointcut>的id 也就是切入点

最后去写一个类进行测试:

MyTest:

import cn.mianju.demo01.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class MyTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.add();
    }
}

注意:getBean的时候的参数是接口 不是UserServiceImpl

2.1.2 方式二:自定义类来实现AOP

首先这个自定义类来实现 切面 相对于第一张 Spring的API实现简单很多 但是功能相对于不够全面

少了 method、args、target、returnValue 等参数

在简单环境下可以使用这种方式

首先自定义一个类:

package cn.mianju.demo01.diy;
​
public class MyDiy {
​
    void before(){
        System.out.println("========运行之前========");
    }
​
    void after(){
        System.out.println("========运行之后========");
    }
}

其中2个方法是需要在我模块运行前后插入的

然后就可以到 applicationContext.xml 中去注册:

<?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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
​
<!--        注册Bean-->
    <bean id="userService" class="cn.mianju.demo01.service.UserServiceImpl"/>
    <bean id="diy" class="cn.mianju.demo01.diy.MyDiy"/>
​
    <aop:config>
​
<!--        还是获取切入点 和之前一样-->
        <aop:pointcut id="point" expression="execution(* cn.mianju.demo01.service.UserServiceImpl.*(..))"/>
​
        <aop:aspect ref="diy">
            <aop:after method="after" pointcut-ref="point"/>
            <aop:before method="before" pointcut-ref="point"/>
        </aop:aspect>
​
    </aop:config>
​
</beans>

在这里要注意:

  • 切入点一样要获取 需要在哪个方法添加这几个功能就切入到哪个地方

  • 使用的是 <aop:aspect> 标签 也就是 切面 标签 属性 ref指定自定义的类

  • 然后在<aop:aspect>标签中 有前后方法的标签

    • <aop:after>:在方法执行前执行

    • <aop:before>: 在方法执行后执行

注意 这种方式简单一些 但是没有获取到 method、args、target、returnValue 等参数

如果需要这些参数还是使用方法一来实现AOP

2.2 注解实现AOP

最后一种实现AOP的方式: 注解方式实现

注解实现AOP和方式二:自定义类实现AOP比较类似 都需要定义注册一个自定义的 切面类

只是一个是在XML中配置 一个是使用注解去实现

首先也需要一个自定义一个切面类:

package cn.mianju.demo02.diy;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
​
@Aspect //声明是一个切面类
public class MyZhujieDiy {
​
    @Before("execution(* cn.mianju.demo02.service.UserServiceImpl2.*(..))")
    //Before:是代码运行之前运行
    void before(){
        System.out.println("==前==");
    }
​
    @After("execution(* cn.mianju.demo02.service.UserServiceImpl2.*(..))")
    //after : 代码运行之后运行
    void after(){
        System.out.println("==后==");
    }
​
    @Around("execution(* cn.mianju.demo02.service.UserServiceImpl2.*(..))")
    //环绕运行 :运行前后运行后都可以运行一些代码
    void around(ProceedingJoinPoint point){
        System.out.println("环绕前");
        
        try {
            //执行方法 比如add()
            Object proceed = point.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        
        System.out.println("环绕后");
    }
​
}

这段话的注解有:

  • @Aspect: 声明是一个切面类、自定义的AOP类

  • @Before:方法执行之前运行

  • @After:方法执行之后运行

  • @Around:环绕运行 可以写执行前也可以写执行后 中间执行的方法就是 point.proceed()

注意 这段代码的结果是:

环绕前
==前==
add
==后==
环绕后

也就是说 环绕运行的优先级最高 然后中途 运行代码的时候 也就是到 Object proceed = point.proceed(); 这句话的时候 才相当于开始执行add那个方法 才会执行befoce和after

XML中的配置也不需要再配置过多、过于臃肿:

<?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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
​
    <bean id="userService2" class="cn.mianju.demo02.service.UserServiceImpl2"/>
    
<!--    这个自定义的也需要注册-->
    <bean id="zhujie" class="cn.mianju.demo02.diy.MyZhujieDiy"/>
    <!--    开启注解支持-->
    <aop:aspectj-autoproxy/>
​
</beans>
  • AOP的约束一定要有

  • 自定义的切面类也需要注册

  • 需要开启AOP注解的支持(否则虽然不会报错 但是AOP失效)

3、 小结

首先 AOP是:可以在不影响我们业务的情况下 实现动态的增强

AOP的实现有三种

  • Spring API实现

    • 相对繁琐 但是更全面 通过XML来配置 也可以获得 target对象等属性

  • 自定义切面类实现

    • 相对简单 只需要创建一个切面,编写需要插入的逻辑代码后,在XML中注册并配置 适用于简单业务

  • 注解实现

    • 自定义切面类实现 类似;不过更简单 在xml中开启注解说明后 只需要创建一个切面类 在对应的位置添加注解后就可以实现

    • 但是和自定义切面类一样 不适应与复杂业务 因为都没有target对象等重要属性

按我的理解:AOP就是一个类似装饰器的东西,在不修改原本的代码下进行动态的代码的拓展;底层的实现用到了动态代理和反射机制

文章作者: 面具
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MianJu —— 这只是一个 Title 而已~
spring 面试 java spring java
喜欢就支持一下吧