学习目标
- 能够理解AOP基本概念和应用场景
- 能够掌握Spring的AOP注解的实现
- 能够实现AOP配置的日志切面案例
场景导入
目前项目接近尾声,基于产品经理的要求,需要来统计每个业务调用消耗的时间,据统计业务方法大约有100多个,如果需要统计耗时,需要在每个业务方法的加耗时统计代码,那如何能快速的为这些业务方法加耗时统计呢?那接下来就会用到Spring中的AOP知识。
一. AOP的基本概念
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的
基础上,对我们的已有方法进行增强。方法的拦截器。批量方法功能的动态增强。
1. 面向对象OOP
(Object Oriented Programming 面向对象编程)
- 三大特征:封装、继承和多态
- 比如说,有DOG类、Cat类、Horse类,它们都有eat方法,run方法,按照OOP的编程思想,那么我们可以抽象出父类Animal,在父类中放置相同的属性或者方法,这样来避免多子类中重复的代码。
2. 面向切面AOP
(Aspect Oriented Programming )
如上图所示:
- 横切逻辑往往在很多方法中,它是具有重复性的
- 横切逻辑和业务代码混杂在一起,让业务代码变得很臃肿,业务代码应该只处理业务逻辑
- OOP已经不能处理横切逻辑的问题了,AOP横空出世,AOP独辟蹊径的提出了横向抽取机制,将业务逻辑和横切逻辑分离,但分离不是难事,难的是分离之后怎么把横切逻辑再融合到原有业务逻辑上,达到和原来一样的效果?
3. AOP的编程应用场景
场景名称 | 场景描述 |
---|---|
事务控制 | 批量的为业务对象,增加事务处理的功能,不用每个对象都去做事务处理 |
记录跟踪 | 批量的为业务对象,增加日志记录的功能,不用在每个方法执行进行日志记录 |
权限控制 | 批量的为业务对象,增加权限检查,不用每个业务对象都做安全检查 |
异常处理 | 批量的为业务对象,增加异常情况的处理功能,不用在每个方法执行进行异常处理 |
参数校验 | 批量的为业务对象,增加各种入参的检查,不用在方法内部进行检查 |
等等 | … |
结论:AOP就是能动态的批量的为一些对象增强功能,这些功能最终是通过代理对象来封装。最终通过调用代理对象来完成真实业务调用,代理对象功能=代理增强功能+原对象原功能。
二. 如何动态增强对象功能
1.思考两个问题
1.如何为已存在的对象,动态增强功能?
2.如何为已产生的Bean对象,动态增强功能?
2.为已存在的对象,增强功能
如果一个对象是基于接口实现的,可以使用JDK动态代理实现。
拷贝之前的账户增删改查案例代码
3.为已存在的Spring Bean对象,增强功能
目前使用的是Spring框架,其业务对象是通过Spring容器产生的,如何为已经产生的SpringBean对象增强功能,可以使用Spring扩展点BeanPostProcessor
package com.itheima.processor;
import com.itheima.service.AccountService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (AccountService.class.isAssignableFrom(bean.getClass())){
//使用动态代理偷偷修改原本的实现类逻辑
Object o = Proxy.newProxyInstance(MyBeanPostProcessor.class.getClassLoader(),
bean.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(bean, args);
long end = System.currentTimeMillis();
System.out.println("正在执行:"+method.getName()+"方法---花费了:"+(end-start)+"毫秒");
return result;
}
});
return o;
}
return bean;
}
}
package com.itheima.processor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//System.out.println("\n");
//System.out.println("我是上头派来的巡查员 来康康你们!!");
//System.out.println("正在被巡视的对象:"+bean);
//可以判断的!!!!
//bean.getClass() 获取被巡查对象的类
//bean.getClass().isAnnotationPresent(Service.class) 判断被巡查对象的类是否包含有service注解
if (bean.getClass().isAnnotationPresent(Service.class)){
//System.out.println("我逮着你了!!!");
//service不是我心中所想!!!
//使用动态代理技术 修改这个对象
/**
*
* 第一个参数 随便自己写类获取到类加载器就可以用
* 第二个参数 改变某个对象 还必须跟原来对象实现同样的接口
* bean.getClass().getInterfaces 获取原本对象它所有实现所有接口们
* 第三个参数 就是您使用动态代理技术创建出来这个对象 真正执行的方法都是由它来执行 invocationHandler
*
*
*/
Object newProxyInstance = Proxy.newProxyInstance(
MyBeanPostProcessor.class.getClassLoader(),
bean.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//System.out.println("动态代理对象正在执行"+method.getName()+"方法");
//调用原本的方法!!!!
//声明一下原本方法的返回值
Object result=null;
System.out.println("原本对象就要执行方法了***************\n");
//传参
//第一个参数 原本对象
//第二个参数 运行的时候需要参数
result=method.invoke(bean,args);
System.out.println("原本对象的方法执行完了***************\n");
return result;
}
}
);
//返回的意思 告诉框架 原来扔了 用我这个!!!
return newProxyInstance;
}
return null;
}
}
三.spring中的AOP编程
上面自定义AOP,是一种编程式的AOP,使用Spring的后置处理器,动态的生成代理对象
接下来学习 spring 的 AOP,就是通过配置的方式,实现上一章节的AOP功能,这个AOP是Spring的AOP!
Spring的AOP,强大之处在于不需要通过代码,只需要使用注解或XML即可完成同样的功能!
1. Aspect(切面):
- 切面由切点和增强组成。
- 切面=切点+增强
2. Pointcut(切点)
为了增强某个业务功能,比如公共日志,比如公共事务,我们需要创建一个标志,代表这个增加。它最终代表是哪些方法可以执行这些增强的功能。我们把这个标志称之为一个切点。
3. Advice(通知/增强):
- 增强的第一层意思是横切逻辑代码(增强逻辑代码)
- 增强的第二层意思是把横切逻辑织入到哪些方法的方法前/后等
3.1 前置通知(before)
作用:用于配置前置通知。指定增强的方法在切入点方法之前执行
属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
执行时间点:切入点方法执行之前执行
3.2 后置通知(afterReturn)
作用:用于配置后置通知。指定增强的方法在切入点方法之后执行
属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
执行时间点:切入点方法正常执行之后。抛出异常就不执行了。
3.3 异常通知(afterThrowing)
作用:用于配置异常通知,方法抛出异常执行
属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个
3.4 最终通知(after)
作用:用于配置最终通知,在方法执行完毕的最后执行
属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
执行时间点:无论切入点方法执行时是否有异常,它都会在其后面执行。
3.5 AOP 环绕通知(around)
作用:用于配置环绕通知。它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
注意:通常情况下,环绕通知都是独立使用的
四.基于注解的AOP编程例子(掌握)
目标: 为所有的业务层的方法批量添加 日志功能
1. 添加拷贝之前的项目
2. 添加maven依赖
<!--添加aop的依赖支持-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
3. 添加开启注解驱动
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan({"com.itheima.service","com.itheima.logger"})
public class Config {
}
或者
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.itheima.service,com.itheima.logger"/>
<!--开启注解驱动 支持注解-->
<aop:aspectj-autoproxy/>
</beans>
4 编写切面对象
package com.itheima.logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 切面对象=切点+增强逻辑(通知逻辑)
*/
@Component
@Aspect
public class ServiceAop {
/**
* 这就是切点信息 表示对哪些方法感兴趣*/
@Pointcut("execution(* com.itheima.spring.service.impl.*.* (..))")
public void pt1(){
}
@Before("pt1()")
public void beforePrintLog(){
System.out.println("@Before在方法之前执行");
}
@AfterReturning("pt1()")
public void afterReturnPrintLog(){
System.out.println("@AfterReturning,有异常就不执行");
}
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("@AfterThrowing在方法抛出异常之后执行");
}
@After("pt1()")
public void afterPrintLog(){
System.out.println("@After 在方法最后执行,不管有没有异常都执行");
}
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
System.out.println("在方法之前执行");
obj = joinPoint.proceed();//调用方法,这相当于调用被代理类的被增强的方法。有点类似,method.invoked()
System.out.println("在方法之后执行,有异常就不执行");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("在方法抛出异常之后执行");
} finally {
System.out.println("在方法最后执行,不管有没有异常都执行");
}
return obj;
}
}
package com.itheima.logger;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
*
* 写增强的逻辑的增强逻辑 切面类 通知类
* 抽取一些增强逻辑方法
*
*
*/
@Component
//注解含义就是向spring说明 我这个类是是个切面类
@Aspect
public class LoggerAspect {
/**
* 无论 @Before @After ......
* 注解有参数 表达式
* 表示你对那些类的那些方法感兴趣 切入点表达式
*
* execution(方法修饰符 返回值类型 包名.包名.包名.包名.类名.方法名(参数类型) )
* 方法修饰符号 省略不写了!
* 返回值类型 一般都是写 *
*
* 后面的包名省略* 多级省略用..
* 参数也一样 多级省略.. 参数没有*
*
*/
@Pointcut("execution( * com.itheima.service..*.*(..))")
public void xyz(){
//这个方法的名字 无所谓 叫啥都行
}
// //@Before("execution( * com.itheima.service..*.*(..))")
// @Before("xyz()")
// public void logBefore(){
// System.out.println("原本对象就要执行方法之前***************");
//
// }
// //@AfterReturning("execution( * com.itheima.service..*.*(..))")
// @AfterReturning("xyz()")
// public void logCommon(){
// System.out.println("原本对象的方法之后正常运行了 会执行了....");
//
// }
// //@After("execution( * com.itheima.service..*.*(..))")
// @After("xyz()")
// public void logFinal(){
// System.out.println("原本对象的方法执行最终**************");
//
// }
//
//
// //@AfterThrowing("execution( * com.itheima.service..*.*(..))")
// @AfterThrowing("xyz()")
// public void logException(){
// System.out.println("方法执行的时候出现异常了");
// }
//环绕通知(增强)
@Around("xyz()")
/**
* joinPoint 就是对原本对象的方法封装
*/
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result=null;
System.out.println("环绕通知前....");
try {
//有原本的方法吗?
Object[] args = joinPoint.getArgs();
System.out.println("获取原本对象的执行那一刻传递的参数!!!"+ Arrays.toString(args));
Class<? extends ProceedingJoinPoint> pointClass = joinPoint.getClass();
System.out.println("原本对象的类:"+pointClass);
Signature signature = joinPoint.getSignature();
System.out.println("原本对象的方法签名:"+signature);
//注意事项!!!!
//1.既然搞不清原本方法是否有返回值 那么 默认都有!!!
result = joinPoint.proceed();
System.out.println("环绕正常运行....");
}catch (Throwable e){
System.out.println("环绕发生异常....");
//注意事项!!!!
//万一原本的方法执行了 但是有异常?
//一种 真的do no thing 基本不会这么玩
//二种 抛出去!!!
throw e;
}finally {
System.out.println("环绕通知最终都会执行....");
}
return result;
}
}
//切入点表达式说明
//表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
//1.全匹配方式:
//public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.pojo.Account)
//2.访问修饰符可以省略
//void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.pojo.Account)
//3.返回值可以使用*号,表示任意返回值
//* com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.pojo.Account)
//4.包名可以使用*号,表示任意包,但是有几级包,需要写几个*
//* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.pojo.Account)
//5.使用..来表示当前包,及其子包
//* com..AccountServiceImpl.saveAccount(com.itheima.pojo.Account)
//6.类名可以使用*号,表示任意类
//* com..*.saveAccount(com.itheima.pojo.Account)
//7.方法名可以使用*号,表示任意方法
//* com..*.*( com.itheima.pojo.Account)
//8.参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
//* com..*.*(*)
//9.参数列表可以使用..表示有无参数均可,有参数可以是任意类型
//* com..*.*(..)
//10.全通配方式:
//* *..*.*(..)
//注:
//通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
//execution(* com.itheima.service.impl.*.*(..))
五.基于xml的AOP编程例子(了解)
1. 拷贝之前项目
2 还原之前项目注解配置
删除 @aspect @Pointcut @Before @AfterReturning @AfterThrowing @After @Around 注解
3 修改配置文件
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置日志通知(增强)工具对象,交由Spring ioc容器管理-->
<bean id="logger" class="com.itheima.logger.Loggerx"/>
<!--
<aop:config>标签作用:配置AOP
-->
<aop:config>
<!--
<aop:aspect> 标签作用:配置切面
id属性:设置当前切面的唯一标识
ref属性:指定当前切面的增强类(日志增强工具类)
-->
<aop:aspect id="loggerAdvice" ref="logger">
<!--
配置切入点
-->
<aop:pointcut id="logPointcut" expression="execution(public * com.itheima.service.impl.AccountServiceImpl.*(..))"/>
<!--
配置前置增强:在方法执行前执行
method属性:前置增强调用日志增强工具类中的那个方法。
pointcut属性:配置切入点,也就是对那个方法进行增强。
-->
<aop:before method="beforePrintLog" pointcut-ref="logPointcut"/>
<!--
配置配置后置通知:在方法执行完毕之后执行(即便抛出异常还是执行)
-->
<aop:after-returning method="afterReturnPrintLog" pointcut-ref="logPointcut"/>
<!--
配置异常通知增强:在方法抛出异常后执行
-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="logPointcut"/>
<!--
配置最终通知增强:在方法执行后执行
-->
<aop:after method="afterPrintLog" pointcut-ref="logPointcut"/>
<!--配置环绕通知-->
<!--<aop:around method="aroundPrintLog" pointcut-ref="logPointcut"/>-->
</aop:aspect>
</aop:config>
</beans>
运行结果