5 Spring AOP
AOP概述
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。Aop的作用:在程序运行期间,不修改源码对已有方法进行增强。Aop优势:减少重复代码、提高开发效率、维护方便AOP的实现方式:使用动态代理技术
Spring AOP常用术语
Joinpoint( 连接点)
所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。即实际增强功能的方法
Pointcut( 切入点)
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。一个类中所有的方法都可以是切入点,Spring Aop中切入点只能是方法。所以我么你可以把切入点理解成:目标对象里面所有的方法都称为切入点
Advice( 通知/ 增强)
Advice可以翻译成通知,也有人称为增强。所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。即我们要给目标方法实际提供的增强代码就是增强。那么根据增强代码书写的位置不同,增强可以分为以下几类:
前置通知,后置通知,异常通知,最终通知,环绕通知。
Target( 目标对象)
要被增强功能的对象就是目标对象。
Proxy(代理)
一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect( 切面)
Aspect称为切面。切面是切入点和增强的结合。通过代码会发现,增强功能所在的类称为切面类,里面配置的是增强和切入点的都是切面
案例:Spring AOP
-
pom.xml引入AOP的依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.2.25.RELEASE</version> </dependency>
-
定义目标对象
@Component public class Calculator {public int add(int num1,int num2){int sum = num1 + num2;return sum;}public int div(int num1,int num2){int sum = num1 / num2;return sum;} }
-
定义切面类,增强功能所在的类称为切面类
@Component @Aspect //切面 public class LoggerAdvice {public void before(){System.out.println("xxx方法被调用了,传入的参数是:...");}public void after(){System.out.println("xxx方法执行完毕了,执行结果是:...");}public void afterThrowing(){System.out.println("xxx方法被执行出错了,出现的异常是:...");} }
-
指定通知类型,通知就是指实际要增强的代码
@Component @Aspect //切面 public class LoggerAdvice {//指定增强代码实际织入位置:切入点代码执行前// @Before(一个程序中有N个类,具体想给哪些类做增强) //定义切入点@Before("execution(* com.woniu.demo.Calculator.add(int,int))")public void before(){System.out.println("xxx方法被调用了,传入的参数是:...");}//指定增强代码实际织入位置:切入点代码执行后@AfterReturning("execution(* com.woniu.demo.Calculator.add(int,int))")public void after(){System.out.println("xxx方法执行完毕了,执行结果是:...");}//指定增强代码实际织入位置:切入点代码执行出错时@AfterThrowing("execution(* com.woniu.demo.Calculator.add(int,int))")public void afterThrowing(){System.out.println("xxx方法被执行出错了,出现的异常是:...");} }
-
在Spring的配置类上开启spring aop的代理模式,并选择代理方式
@Configuration //通知spring容器,这个类与别的类不一样,是一个配置类,用于初始化spring容器的一个类@ComponentScan(basePackages = {"com.woniu.dao","com.woniu.service","com.woniu.controller","com.woniu.target"}) @EnableAspectJAutoProxy(proxyTargetClass = true) //开启aop代理 proxyTargetClass选择代码模式true:cglib false:jdk代理 public class SpringConfig { }
proxyTargetClass默认值是false,代表的是如果目标类是有接口的使用jdk的proxy代理,如果没有接口使用cglib;如果将proxyTargetClass设置为true,那么不管目标类是实现了接口,都会使用cglib进行代理。
-
测试代理对象,观察增强功能是否增强成功
//1.通知测试类,使用的spring的测试 @RunWith(SpringJUnit4ClassRunner.class) //通知测试类基于spring提供的单元测试 //注解里面赋值,省略属性名,属性值赋值value,class指定配置类的字节码 @ContextConfiguration(classes = SpringConfig.class)//指定测试时,基于哪个spring的配置文件测试 public class MySpringTester {@Autowiredprivate Calculator calculator; //最终注入时就是代理对象@Test //java的junit框架提供的注解public void test03(){//输出:class com.woniu.demo.Calculator$$EnhancerBySpringCGLIB$$d89ada67System.out.println(calculator.getClass());calculator.add(1,1);} }
测试效果如下所示:
在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
切入点表达式【会读会用】
语法:
execution( [权限修饰符] 返回值类型 包名.类名.方法名(参数列表) )
举例:execution(public int com.woniu.Calculator.div(int,int))简化execution(int com.woniu.Calculator.div(int,int))
提示:语法中出现[]包裹的内容都是可选的
常见的切入点案例
1. execution(public * *(..)) 所有的public的方法2. execution(* com.spring.aop.*(..))所有的aop包下的所有类的方法(不包含子包) 3. execution(* com.spring.aop..*(..))所有的aop包及其子包下的所有类的方法.切入点表达式:execution(* com.woniu..*(..)) *表示返回值类型、类名、方法名不限 (..)参数列表不限制4. execution(* com.spring.aop.IOrderService.*(..))IOrderService接口中定义的所有方法5. execution(* com.spring.aop.IOrderService+.*(..))匹配实现特定接口所有类的方法 IOrderServiceIOrderService+ 举例:IOrderService1 IOrderServiceImpl 不能匹配IOrderService IOrderService* 举例:IOrderService1 IOrderServiceImpl 匹配IOrderService 6. execution(* save*(..))去匹配所有以save开头的方法 save save1 save2
AOP增强分类
前置通知@Before
作用 用于配置前置通知。指定增强的方法在切入点方法之前执行
属性:method:用于指定通知类中的增强方法名称ponitcut-ref:用于指定切入点的表达式的引用poinitcut:用于指定切入点表达式
执行时间点: 切入点方法执行之前执行
后置通知@AfterReturning
作用:用于配置后置通知
属性:method:指定通知中方法的名称。pointct:定义切入点表达式pointcut-ref:指定切入点表达式的引用
执行时间点:切入点方法正常执行之后。它和异常通知只能有一个执行
异常通知@AfterThrowing
作用:用于配置异常通知,切入点方法发生异常时才执行属性:method:指定通知中方法的名称。pointcut:定义切入点表达式pointcut-ref:指定切入点表达式的引用
执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个
注意:目标行为只有抛出了异常之后才会执行这个增强方法
最终通知@After
作用:用于配置最终通知
属性:method:指定通知中方法的名称。pointct:定义切入点表达式pointcut-ref:指定切入点表达式的引用
执行时间点: 无论切入点方法执行时是否有异常,它都会在其后面执行。
无论是否有异常,最终通知都会执行
案例1:利用前置、后置、异常和最终增强完成日志管理
-
目标类
@Component public class Calculator implements ICalculator{public int sum(int num1,int num2){System.out.println("......目标方法sum执行");int sum = num1 + num2;return sum;}public int sub(int num1,int num2){int result = num1 - num2;return result;}public int plus(int num1,int num2){int result = num1 * num2;return result;}public int div(int num1,int num2){int result = num1 / num2;return result;} }
-
切面类
@Aspect @Component public class LoggerAdvance {//抽取公共切入点@Pointcut("execution(* com.woniu.target..*(..))")public void myPointCut(){}/**** @param jp 连接点,AOP实际增强功能的哪个方法 不用程序员传值,spring aop赋值*///前置增强(切入点表达式)@Before("myPointCut()")public void before(JoinPoint jp){//方法签名Signature signature = jp.getSignature();String methodName = signature.getName();//实参列表Object[] args = jp.getArgs();System.out.println(methodName+"方法被调用了,用户传入的参数分别是:"+ Arrays.toString(args));}/**** @param jp* @param result @AfterReturning后置增强,用来接收即将返回的值,程序员不管,spring aop注入*/@AfterReturning(value = "myPointCut()",returning = "result")//returning属性作用将方法执行结果注入程序员指定的参数中public void afterReturning(JoinPoint jp, Object result){System.out.println(jp.getSignature().getName()+"方法执行完毕,执行结果是:"+result);}/*** 异常增强,切入点方法执行出错时,实现的增强* @param e 用来保存spring aop做异常增强时,实际发生的异常对象*/@AfterThrowing(value = "myPointCut()",throwing = "e")public void afterThrowing(JoinPoint jp,Throwable e){System.out.println(jp.getSignature().getName()+"执行出错啦,出现的错误是:"+e);}/*** 最终增强,切入点方法执行过程不管有没有异常,始终会执行的代码 finally{}中的代码*/@After("myPointCut()")public void after(){System.out.println("无论代码是否出错,你都可以看到这行输出语句 释放哪些资源");} }
-
测试代码
@Autowired private ICalculator proxy; //代理对象 JDK:代理类 implements ICalculator CGLIB extends Calculator @Test public void mt02(){proxy.div(12,0); }
使用@Pointcut注解定义公用切入点
在每一个通知中定义切点,工作量大,不方便维护,我们可以使用@pointcut来声明切点
切点允许逻辑运算 例如mypointcut()||mypointcut1()
6 环绕增强
环绕通知@Around
环绕增强功能是spring aop五种增强里面最厉害!!使用最灵活!!
作用:环绕通知,它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
属性:method:指定通知中方法的名称。pointcut:定义切入点表达式pointcut-ref:指定切入点表达式的引用
注意:通常情况下,环绕通知都是独立使用的
案例:利用环绕增强完成日志管理
-
目标类
@Component public class Calculator implements ICalculator{public int sum(int num1,int num2){System.out.println("......目标方法sum执行");int sum = num1 + num2;return sum;}public int sub(int num1,int num2){int result = num1 - num2;return result;}public int plus(int num1,int num2){int result = num1 * num2;return result;}public int div(int num1,int num2){int result = num1 / num2;return result;} }
-
切面类
@Component @Aspect public class LoggerAdvicer {@Around("execution(* com.woniu.demo..*(..))")public Object around(ProceedingJoinPoint pjp){try {System.out.println(pjp.getSignature().getName()+"方法被调用了xxx传入的参数是:"+ Arrays.toString(pjp.getArgs()));//指定目标对象的方法正在执行Object result=pjp.proceed(pjp.getArgs());System.out.println(pjp.getSignature().getName()+"方法执行完毕了xxx执行结果是:"+result);return result;} catch (Throwable e) {System.out.println(pjp.getSignature().getName()+"方法被执行出错了xxx出现的异常是:"+e);throw new RuntimeException(e);} finally {System.out.println(pjp.getSignature().getName()+"方法不管有没有异常xxx都会执行的代码");}} }
-
测试代码
@Autowired private Calculator calculator; //最终注入时就是代理对象 @Test //java的junit框架提供的注解 public void test03(){System.out.println(calculator.getClass());calculator.add(1,0);//连接点 }
运行测试代码,控制台输出add方法的日志增强代码:
经验分享:
Spring Aop应用的场景
日志记录,性能统计,安全控制,事务处理,异常处理等等。
利用AOP可以将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非主导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP与OOP区别
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
换而言之,OOD/OOP面向名词领域,AOP面向动词领域。
学习 spring中的aop要明确的事
a 、开发阶段(我们做的):编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。b 、运行阶段(Spring 框架完成的):Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
7 spring AOP完成事务管理
引入案例:转账业务
需求:基于atm数据库的cardInfo表完成两个账户之间的转账业务
参考代码
-
CardInfoDao.java完成转账sql
@Repository public class CardInfoDaoImpl implements CardInfoDao {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic int updateIn(String cardId, BigDecimal inMoney) {return jdbcTemplate.update("UPDATE cardinfo SET balance=balance+? WHERE card_id=?",inMoney,cardId);}@Overridepublic int updateOut(String cardId, BigDecimal outMoney) {return jdbcTemplate.update("UPDATE cardinfo SET balance=balance-? WHERE card_id=?",outMoney,cardId);} }
-
CardInfoService.java完成转账业务
@Service public class CardInfoServiceImpl implements CardInfoService {@Autowiredprivate CardInfoDao cardInfoDao;/*** @param outCardId 转出账户* @param inCardId 转入账户* @param money 交易金额*/@Overridepublic void transfer(String outCardId, String inCardId, BigDecimal money) {cardInfoDao.updateOut(outCardId,money);cardInfoDao.updateIn(inCardId,money);} }
-
测试转账业务
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class MySpringTester {@Autowiredprivate CardInfoService cardInfoService;@Testpublic void query() {cardInfoService.transfer("2394916393","2416932602",new BigDecimal(100));} }
运行代码,两个账户金额发生变化:账户“2394916393”金额更新为900,账户“2416932602”金额更新为1100,转账成功!!
我们尝试将CardInfoDao的sql故意写错试一试:@Overridepublic int updateIn(String cardId, BigDecimal inMoney) {return jdbcTemplate.update(//此处ST是故意写错的"UPDATE cardinfo ST balance=balance+? WHERE card_id=?",inMoney,cardId);}
运行代码,两个账户金额依然发生变化:账户“2394916393”金额更新为800,账户“2416932602”金额却依然是1100,转账貌似有一个”成功了“,这种现象符合实际转账功能的需求么????当然不合理!!!转账是一定要保证:两个用户的钱同时变化的。所以转账业务有问题!怎么解决呢?依靠数据库的事务管理。
spring事务控制我们首先要认知到以下几个点:
第一:JavaEE 体系进行分层开发,事务处理位于业务层,Spring 提供了分层设计 业务层的事务处理解决方案。
第二:spring 框架为我们提供了一组事务控制的接口。具体在后面的第二小节介绍。这组接口是在spring-tx-xx.jar 中。
第三:spring 的事务控制都是基于 AOP 的,它既可以使用编程的方式实现,也可以使用配置的方式实现。我们学习的重点是使用配置的方式实现。
spring中事务管理机制依靠aop实现,那么完成事务管理也是遵循aop开发流程:首先我们需要定义事务管理的切面类,切面里面写什么代码呢?回忆之前自己管理事务,在核心业务代码基础上就是要提供事务提交、回滚相关的功能,
- 事务切面类
- commit
- rollback
- 。。。。
这个所谓的事务切面类,在spring中其实已经定义并提供了,称为事务管理器。
在转账案例中我们已经知道了spring中存在的事务问题,接下来我们使用aop技术让spring自动帮我们控制事务。
Spring 中事务控制的 API 介绍
- PlatformTransactionManage:此接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法,如下图:
因为PlatformTransactionManage是一个接口,并没有提供实际的方法实现。Spring为什么会抽取这样一个接口呢?有一个原因我们可以了解一下:
现下持久层框架众多,Spring没有办法提供一套事务的实现能够满足各大持久层框架的需求,比如MyBatis和Hibernate两大持久层框架,实现机制区别很大。所以spring的想法就是我把功能抽取出来,项目中结合自己实际使用的持久层来提供对应的代码实现:
案例:基于spring aop完成事务管理处理转账业务
分析:
我们现在的案例使用的持久层框架是spring内置的Spring-jdbc,所以事务切面类就应该根据上图指示,使用“DataSourceTransactionManager”。这个实现类spring-jdbc已经提供
我们要做的事情,就是让自己项目的spring容器可以扫描到这个事务管理类,但是因为这个类不是我们自己编写,无法直接添加@Component注解,只能在SpringConfig配置类中利用@Bean注入到spring容器中,详细代码如下所示:
-
pom.xml导入事务管理的依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId><version>5.2.25.RELEASE</version> </dependency>
-
SpringConfig配置类:配置DataSourceTransactionManager事务管理器对象,利用@EnableTransactionManagement开启事务管理
@ComponentScan(basePackages = {"com.woniu.dao","com.woniu.service","com.woniu.controller","com.woniu.target","com.woniu.advance"}) //spring不管理实体类entity @PropertySource("classpath:db.properties") //开启spring AOP代理 proxyTargetClass默认值是false,就是JDK true启用cglib @EnableAspectJAutoProxy(proxyTargetClass = true) //开启spring aop动态代理 @EnableTransactionManagement(proxyTargetClass = true) //开启spring事务管理 @Import({JdbcConfig.class}) //导入其他的配置类 public class SpringConfig {@Beanpublic JdbcTemplate jdbcTemplate(HikariDataSource dataSource){JdbcTemplate jdbcTemplate=new JdbcTemplate();jdbcTemplate.setDataSource(dataSource);return jdbcTemplate;}/*** 事务管理器,即利用spring AOP管理事务时的增强类* @param dataSource* @return*/@Beanpublic DataSourceTransactionManager dataSourceTransactionManager(HikariDataSource dataSource){DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);return dataSourceTransactionManager;} }
-
业务层哪个方法需要事务管理,添加spring提供事务注解@Transactional
@Service public class CardInfoServiceImpl implements CardInfoService {@Autowiredprivate CardInfoDao cardInfoDao;//注解:spring提供注解@Transactional@Overridepublic void transfer(String inCardId, String outCardId, BigDecimal money) {cardInfoDao.updateOut(outCardId,money);cardInfoDao.updateIn(inCardId,money);} }