面向切面编程
Ioc使软件组件松耦合。AOP让你能够捕捉系统中 经常使用的功能,把它转化成组件。
AOP(Aspect Oriented Programming):面向切面编程(AOP是一种编程技术)
切面就是:程序中,和核心业务逻辑没有关系的通用代码
AOP是对OOP的补充延伸。
AOP底层使用的是 动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理+CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果是代理某个类,这个类没有实现接口,就会切换使用CGLIB。 也可通过一些配置使得spring只使用CGLIB。
老杜说的面向切面编程:将业务逻辑中通用的非业务逻辑代码单独提取出来(比如日志、安全、事务),形成一个横向的切面,把核心业务逻辑看出是纵向的,我们以横向 交叉的方式 应用到业务流程当中的过程====AOP 后面说得怪怪的。。。 (通用的非业务逻辑的代码==交叉业务
1.AOP介绍
一般一个系统中都会有一些系统服务,例如:日志、事务管理、安全等。
这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全 这些都是要做的。
像事务:事务的代码永远是那三部分:开启事务,提交事务,(遇到异常)回滚事务
如果在每一个业务处理过程中,都掺杂这些交叉业务代码进去的话,会存在两方面问题:
*一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处
*二:程序员无法专注于核心业务代码的编写,在编写核心业务代码的同时还需处理这些交叉业务
使用 AOP(面向切面编程--一种技术) 可以轻松解决以上问题。
看下图, 理解AOP思想
用一句话总结AOP:将与核心业务无关的代码 独立地抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
1.代码复用性增强
2.代码易维护
3.使开发者更关注业务逻辑。
2.AOP的七大术语
连接点 JoinPoint
在程序的整个执行流中,可以织入切面的位置。方法的执行前后,异常抛出之后的位置。
切点PointCut
在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
通知Advice
通知又叫增强,就是具体你要织入的代码
通知包括:前置通知,后置通知,环绕通知,异常通知,最终通知
切面Aspect
切点+通知就是切面
织入Weaving
把通知应用到目标对象上的过程。
代理对象Proxy
一个目标对象被织入通知后产生的新对象
目标对象Target
被织入通知的对象
(白色菱形表示 方法)
连接点 是指哪些 位置 能够让我们 织入(切入)
切点 是指 已经切入的地方,用方法去切入了,切入的方法就是切点
(好像是指核心代码的方法??把核心代码的方法 织入 我们的切面??)
通知 是指 你添加的那些非核心业务的通用代码------增强的代码
3.切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入。
切点表达式的语法格式 :
execution( [ 访问控制权限修饰符 ] 返回值类型 [全限定类名] 方法名(形参列表) [异常] )
访问控制权限修饰符:
可选项(可写可不写)、没写,就是4个权限都包括、写public就表示只包括公开的方法
返回值类型:
必填项; * 表示返回值类型任意
全限定类名:
可选项; 两个点“.." 代表当前包以及子包下的所有类; 省略时表示所有的类
方法名:
必填项; * 表示所有方法; set* 表示所有的set方法
形式参数列表:
必填项; ()表示里面没有参数; (..)参数类型和个数任意的方法;(*)只有一个参数的方法;
(* , String) 第一个参数类型任意,第二个参数类型为 String
异常:
可选项; 省略时表示任意异常类型
4.使用spring的AOP
Spring对AOP的实现共三种方式:
第一种:Spring框架结合AspectJ框架实现的AOP,基于注解方式
第二种:Spring框架结合AspectJ框架实现的AOP,基于XML方式
第三种:Spring框架自己实现的AOP,基于XML方式
实际开发中,都是Spring+AspectJ来实现AOP。所以重点学第一第二种。
什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架使用了AspectJ)
4.1准备工作(注解形式)
使用Spring+AspectJ的AOP需引入的依赖如下:
<!--spring context依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.0-M2</version> </dependency> <!--spring-aspects--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.0-M2</version> </dependency>
Spring配置文件中添加context命名空间和aop命名空间
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> </beans>
4.2实现步骤(注解形式)
书写目标类和目标方法:
package com.spring6.service; @Service public class UserService {//目标类 public void login(){//目标方法 System.out.println("系统正在进行身份认证"); } }
书写切面
package com.spring6.service@Component ------使得能扫描到注解 @Aspect //切面类是需要使用@Aspect注解进行标注的 public class LogAspect {//切面 //切面=切点+通知 // 通知就是增强,就是要编写的具体代码 // 这里通知Advice以方法的形式出现。(因为方法中可以写代码) // @Before注解 标注的方法 就是一个前置通知 ~添before注解,与增强代码形成通知 @Before("execution(* com.spring6.service.UserService.*(..))") public void beforeAdvice(){ | System.out.println("我是一个通知,我是一个增强代码"); > 增强的代码 } _| }
书写spring配置文件:
<?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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 组件扫描--> <context:component-scan base-package="com.spring6.service"/> <!-- 开启aspectj的自动代理--> <!-- spring容器在扫描类时,查看该类上是否有@Aspect注解,如果有,则给这个类生成代理对象--> <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- proxy-target-class="true"表示强制使用CGLIB动态代理; 其默认值是false,表示接口使用JDK动态代理,反之使用CGLIB--> </beans>
书写测试文件:
public class SpringAOPtest { @Test public void testBefore(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.login(); } }
5.通知类型
通知类型包括:
前置通知:@Before 目标方法执行之前的 通知
后置通知:@AfterReturning 目标方法执行之后 的通知
环绕通知:@Around 目标方法之前 添加通知,同时目标方法执行之后 添加通知
异常通知:@AfterThrowing 发送异常之后 执行的通知
最终通知:@After 放在 finally语句块中的通知
//前置通知 @Before("execution(* com.spring6.service..*(..))") public void beforeAdvice() { System.out.println("前置通知,我是一个增强代码"); }测试代码:
@Test public void testBefore(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); OrderService orderService = applicationContext.getBean("orderService", OrderService.class); orderService.generate(); }测试结果:
//后置通知 @AfterReturning("execution(* com.spring6.service..*(..))") public void afterReturningAdvice() { System.out.println("后置通知,我是一个增强代码"); }测试代码:
@Test public void testBefore(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); OrderService orderService = applicationContext.getBean("orderService", OrderService.class); orderService.generate(); }测试结果:
//环绕通知(环绕是最大的通知,在前置通知之前,在后置通知之后) @Around("execution(* com.spring6.service..*(..))") public void aroundAdvice(ProceedingJoinPoint JoinPoint) throws Throwable { // 前面的代码 System.out.println("前环绕"); // 执行目标 JoinPoint.proceed(); // 后面的代码 System.out.println("后环绕"); }测试代码:同上
测试结果:
//最终通知(finally语句块中的通知) @After("execution(* com.spring6.service..*(..))") public void afterAdvice(){ System.out.println("最终通知,这是增强代码"); }测试代码同上
测试结果:
6.切面的顺序
当对同一个核心业务的代码,我们有多个切面要织入时,如何区分各个切面的先后顺序??
使用@Order(阿拉伯数字) 来区分先后顺序
@Service @Aspect @Order(3) public class SecurityAspect {//安全切面 // 通知+切点 @Before("execution(* com.spring6.service..*(..))") public void beforeAdvice(){ System.out.println("前置通知:安全。。"); } }@Component @Aspect //切面类是需要使用@Aspect注解进行标注的 @Order(2) public class LogAspect {//切面
7.通用切点
//前置通知 @Before("execution(* com.spring6.service..*(..))") public void beforeAdvice() { System.out.println("前置通知,我是一个增强代码"); } //后置通知 @AfterReturning("execution(* com.spring6.service..*(..))") public void afterReturningAdvice() { System.out.println("后置通知,我是一个增强代码"); }//最终通知(finally语句块中的通知) @After("execution(* com.spring6.service..*(..))") public void afterAdvice(){ System.out.println("最终通知,这是增强代码"); }上面括号内的 切点表达式 全都一样, ===代码复用差,每个都写一遍==要修改的话全修==
解决办法:写个通用切点
// 定义通用的切点表达式 @Pointcut("execution(* com.spring6.service..*(..))") public void 通用切点(){ // 这个方法只是一个标记,方法名随意,方法体中也不需要写任何代码 }
使用通用切点:
//前置通知 @Before("通用切点()") public void beforeAdvice() { System.out.println("前置通知,我是一个增强代码"); } //后置通知 @AfterReturning("通用切点()") public void afterReturningAdvice() { System.out.println("后置通知,我是一个增强代码"); }
8.连接点Joint Point
//前置通知 @Before("通用切点()") public void beforeAdvice(JoinPoint joinPoint) { System.out.println("前置通知,我是一个增强代码"); // 这个JoinPoint joinPoint,在spring容器中调用这个方法的时候会自动传过来 // 我们可以直接用。用JoinPoint joinPoint干什么? // Signature signature = joinPoint.getSignature(); 这样就能 获取目标方法的签名 // 通过方法的签名能获取到一个方法的具体信息 // 获取目标方法的方法名 System.out.println("目标方法的方法名:"+joinPoint.getSignature().getName()); }
9.Spring AOP 基于注解之全注解开发
@Configuration //代替spring.xml配置文件 @ComponentScan({"com.spring6.service"})//组件扫描 @EnableAspectJAutoProxy(proxyTargetClass = true)//启用AspectJ自动代理机制 public class Spring6Config { }测试代码:
@Test public void testNoXML(){ ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class); OrderService orderService = applicationContext.getBean("orderService", OrderService.class); orderService.generate(); }
10.基于XML方式实现Spring AOP(了解)
package com.spring6.service; public class UserService {//目标对象 public void logout(){//目标方法 System.out.println("系统正在退出,别急"); } }
增强的代码(通知):
package com.spring6.service; import org.aspectj.lang.ProceedingJoinPoint; public class TimerAspect { // 通知 public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { // 前环绕 long begin = System.currentTimeMillis(); // 目标 joinPoint.proceed(); // 后环绕 long end = System.currentTimeMillis(); System.out.println("方法耗时"+(end-begin)); } }
<?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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 纳入spring-ioc--> <bean id="userService" class="com.spring6.service.UserService"></bean> <bean id="timerAspect" class="com.spring6.service.TimerAspect"></bean> <!-- aop配置--> <aop:config> <!-- 切点表达式--> <aop:pointcut id="mypointCut" expression="execution(* com.spring6.service..*(..))"/> <!-- 切面:通知+切点--> <aop:aspect ref="timerAspect"> <aop:around method="aroundAdvice" pointcut-ref="mypointCut"/> </aop:aspect> </aop:config> </beans>
@Test public void testXML(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.logout(); }
11.AOP的实际案例:事务处理
项目中的事务控制是在所难免的。在一个业务流程中,可能需要多条DML语句 共同完成,为保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码,例如以下伪代码:
伪代码:
可以看出,这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的形式,都是:
这个控制事务的代码就是和业务逻辑 没关系的 “交叉业务”。
以上代码中可以看到这些交叉业务的代码没有得到复用,并且如果这些交叉业务的代码需要修改,那必然要修改多处,维护困难。
如何解决?----------采用AOP思想解决。
可以把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。
以上展示怎么做:有两个业务类