Spring(三):AOP编程

Source

Spring(三):AOP编程

1. 静态代理和动态代理

​ AOP编程需要一些关于代理的知识,以便更好地理解AOP编程。代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式。即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.

​ 因为前面已经写了一篇关于代理模式的文章,这里贴出链接。

2. AOP概述

​ AOP(Aspect Oriented Programming),即面向切面编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件系统开发中的一个热点,也是 spring 框架的一个重点。利用 AOP 可以实现业务逻辑各个部分的隔离,从而使得业务逻辑各个部分的耦合性降低,提高程序的可重用性,同时提高开发效率。

2.1 实现原理

​ 在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
​ 如果目标对象实现接口,使用 JDK 代理。
​ 目标对象没有实现接口使用 CGLIB 代理

2.2 术语

  • Joinpoint(连接点):
    在 spring 中,连接点指的都是方法(指的是那些要被增强功能的候选方法),spring 只支持方法类型
    的连接点。

  • Pointcut( 切入点):
    所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。

  • Advice(通知/增强):
    所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
    通知的类型: 前置通知,后置通知,异常通知,最终通知,环绕通知。

  • Target(目标对象):
    被代理的对象。比如动态代理案例中的演员。

  • Weaving(织入):
    织入指的是把增强用于目标对象,创建代理对象的过程。spring 采用动态代理织入,AspectJ 采用编译期织入和类装载期织入。

  • Proxy(代理):
    一个类被 AOP 织入增强后,即产生一个结果代理类。比如动态代理案例中的电脑销售

  • Aspect(切面):
    切面指的是切入点和通知的结合。

3. 基于XML配置的AOP编程

3.1 配置说明

  1. 标签说明
<aop:config>	作用:声明 aop 配置。
<aop:aspect>
	作用:配置切面。
	属性:id:唯一标识切面的名称. ref:引用通知类 bean 的 id
<aop:pointcut>
	作用:配置切入点表达式。
	属性:id:唯一标识切入点表达式名称. expression:定义切入点表达式	


<!--配置通知-->    
<aop:before>			作用:配置前置通知
<aop:after-returning> 	作用:配置后置通知
<aop:after-throwing>	作用:配置异常通知
<aop:after>				作用:配置最终通知
<aop:around>			作用:配置环绕通知
	属性:method:指定通知方法名称
		 pointcut:定义切入点表达式
		 pointcut-ref:引用切入点表达式的 id
  1. 切入点表达式语法
切入点表达式:
0. 语法:
	execution(
	modifiers-pattern?	修饰符(可选)
	ret-type-pattern	返回类型
	declaring-type-pattern?name-pattern(param-pattern)
    		  包名类名(可选)  方法名(参数类型)
	throws-pattern?)
	
1. 全匹配方式
	public void com.itheima.service.impl.AccountServiceImpl.save()
2. 访问修饰符可以省略
	void com.itheima.service.impl.AccountServiceImpl.save()
3. 返回值类型可以使用 * ,表示方法返回任何类型都匹配
	* com.itheima.service.impl.AccountServiceImpl.save()
4. 包名可以使用 * ( 有多少个包使用多少个 *)
	* *.*.*.*.AccountServiceImpl.save()
5. 包名中使用 .., 表示当前包及其子包
	* com..*.AccountServiceImpl.save()
6. 类名称也可以使用 *
	* com..*.*.save()
	或者:
	* com..*.*Impl.save()
7. 方法名称也可以使用 *
	* com..*.*.save*()
8. 方法参数使用 *, 表示任何参数都匹配,但必须有参数
	* com..*.*.save*(*)
9. 方法参数使用 .., 表示任何参数都匹配,可以有参数也可以没有参数
	* com..*.*.save*(..)
10. 其他写法如:
	bean(*Service) 表示 IOC 容器中以 Service 结尾的所有类匹配
  1. 配置步骤
第一步:通过 aop:config 声明 aop 配置
第二步:通过 aop:aspect 配置切面
	id :给切面取一个唯一标识的名称
	ref :指定通知对象的引用。这里是日志通知 bean 的 id
第三步:通过 aop:pointcut 配置切入点表达式
	id :给切入点表达式取一个唯一标识名称
expression :指定切入点表达式
	表达式组成:访问修饰符 返回值 包名称 类名称 方法名称 ( 参数列表 )
第四步:通过 aop:[before|after|after-returning|after-throwing] 配置通知
	method :指定通知方法的名称
	pointcut-ref :指定切入点表达式的 id

3.2 案例:对用户操作织入一些通知

  1. maven工程导入依赖
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.1.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.7</version>
    </dependency>
</dependencies>
  1. UserService接口和实现类
//接口
public interface UserService {
    void save();
    void update();
}

//实现类
public class UserServiceImpl implements UserService{
    @Override
    public void save() {
        System.out.println("保存用户");
    }

    @Override
    public void update() {
        System.out.println("更新用户");
    }
}
  1. 创建切面类
public class Logger {
  
    public void before(){
        System.out.println("前置通知");
    }

    public void afterReturning(){
        System.out.println("后置通知");
    }

    public void afterThrowing(){
        System.out.println("异常通知");
    }

    public void after(){
        System.out.println("最终通知");
}
  1. 在bean.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"
       xmlns:context="http://www.springframework.org/schema/context"
       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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="userService" class="com.zmysna.service.impl.UserServiceImpl"></bean>

    <bean id="logAdvice" class="com.zmysna.utils.Logger"></bean>
    
	<!--声明aop配置--> 	
    <aop:config>
        <!--配置切面--> 
        <aop:aspect id="logAspect" ref="logAdvice">
            <!--配置切入点表达式--> 
            <aop:pointcut id="pt" expression="bean(*Service)"></aop:pointcut>
            <!--配置通知--> 
            <aop:before method="before" pointcut-ref="pt"></aop:before>
            <aop:after-returning method="afterReturning" pointcut-ref="pt">
            </aop:after-returning>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pt">
            </aop:after-throwing>
            <aop:after method="after" pointcut-ref="pt"></aop:after>
        </aop:aspect>
    </aop:config>
    
</beans>
  1. 测试
public class AopTest {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        UserService userService = ac.getBean("userService", UserService.class);
        //可以看出这是一个代理类
        System.out.println(userService.getClass());
        userService.update();
    }
}

​ 测试结果:

	class com.sun.proxy.$Proxy10
	前置通知
	更新用户
	后置通知
	最后通知

4. 环绕通知

​ 环绕通知可以取代前置通知、后置通知、异常通知、最后通知,并且通知顺序不会错乱,环绕通知还可以获得真实对象的信息。我们知道aop编程是通过代理真实对象实现的,可以帮助我们更好地扩展真实对象的功能。

使用环绕通知改造前面的案例

  1. 切面类
public Object around(ProceedingJoinPoint pj){
	//获得真实对象的参数
    Object[] args = pj.getArgs();
    try {
        System.out.println("前置通知");
        //调用真实对象的方法
        Object obj = pj.proceed();
        System.out.println("后置通知");
        //如果有返回值则返回
        return obj;
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("异常通知");
        return null;
    } finally {
        System.out.println("最后通知");
    }
}
  1. bean.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"
       xmlns:context="http://www.springframework.org/schema/context"
       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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="userService" class="com.zmysna.service.impl.UserServiceImpl"></bean>

    <bean id="logAdvice" class="com.zmysna.utils.Logger"></bean>
    
	<!--声明aop配置--> 	
    <aop:config>
        <!--配置切面--> 
        <aop:aspect id="logAspect" ref="logAdvice">
			 <aop:around method="around" pointcut-ref="pt"></aop:around>
        </aop:aspect>
    </aop:config>

</beans>
  1. 测试类和测试结果与前面案例相同。

5. 注解实现AOP编程

5.1 注解说明

注解 说明
@Aspect 指定当前类为切面类,使用在类上面
@Pointcut(切入点表达式) 定义一个切入点表达式,使用在方法上
@Before(切入点表达式) 注解配置前置通知
@AfterReturning(切入点表达式) 注解配置后置通知
@AfterThrowing(切入点表达式) 注解配置异常通知
@After(切入点表达式) 注解配置最终通知
@Around(切入点表达式) 注解配置环绕通知

5.2 使用注解改造案例

  1. 切面类
//指定当前类为切面类
@Aspect
public class Logger {

  	//定义一个切入点表达式
    @Pointcut("bean(*ServiceImpl)")
    public void pt(){}

    @Before("pt()")
    public void before(){
        System.out.println("前置通知");
    }

    @AfterReturning("pt()")
    public void afterReturning(){
        System.out.println("后置通知");
    }

    @AfterThrowing("pt()")
    public void afterThrowing(){
        System.out.println("异常通知");
    }
    
    @After("pt()")
    public void after(){
        System.out.println("最终通知");
    }
}
  1. bean.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"
       xmlns:context="http://www.springframework.org/schema/context"
       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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
    <bean id="userService" class="com.zmysna.service.impl.UserServiceImpl"></bean>

    <bean id="logAdvice" class="com.zmysna.utils.Logger"></bean>
	
    <!--开启注解扫描-->
    <context:component-scan base-package="com.zmysna" />
	<!-- 开启 Aop 自动代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
  1. 测试类和测试结果与前面案例相同。

5.3 继续改造实现纯注解零配置

​ 使用注解改造案例完成后还是会存在配置文件,实现无配置文件实现AOP编程。不过注解虽然方便简化xml配置,但是注解会分散各种类中难以维护,因此要根据不同的情形选择注解和xml,在效率和维护性之间做一个平衡。

​ 接下继续将配置文件的信息用注解来表示,主要是一下两行信息

	<!--开启注解扫描-->
    <context:component-scan base-package="com.zmysna" />
	<!-- 开启 Aop 自动代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
  1. 零配置相关注解
零配置相关注解 说明
@Configuration 表示指定类是一个配置类
@ComponentScan 注解扫描
@EnableAspectJAutoProxy 开启Aop自动代理
  1. 创建一个注解配置类
@Configuration
@ComponentScan("com.zmysna")
@EnableAspectJAutoProxy
public class SpringConfiguration {}
  1. 测试
@RunWith(SpringJUnit4ClassRunner.class) //整合Junit和Spring
@ContextConfiguration(classes = SpringConfiguration.class)  //通过注解配置类创建一个容器
public class FullAnnoTest {

    @Autowired
    private UserService userService;

    @Test
    public void annoTest() {
        System.out.println(userService.getClass());
        userService.update();
    }
}
  1. 测试结果还是不变。