Spring 的AOP
AOP全称(Aspect Oriented Programming)既面向切面编程
通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,AOP是OOP的延续,是软件开发的
热点,也是Spring框架的一个重要内容,是函数式编程的一种衍生,利用AOP对业务逻辑各个部分进行隔离
从而使得逻辑各个部分之间的耦合度降低,提高程序的可重用性,提高开发效率
!!!简单来说就是把我们程序重复的代码抽取出来,需要执行的时候,使用动态代理,在不修改源码的基础上,对已有的方法进行增强!!!
AOP相关术语
Joinpoint(连接点)
所谓连接点是指哪些被拦截到的点,在Spring中,这些点指的是方法, 因为spring只支持方法类型的连接点
Pointcut(切入点)
所谓切入点是指我们对哪些切入点进行拦截的定义
Advice(通知/增强)
所谓通知是拦截到Joinpoint 之后要做的事情就是通知
通知的类型,前置通知,后置通知,异常通知,最终通知,环绕通知
Introduction(引介)
引介是一种特数的通知在不修改类代码的前提下,Introduction可以在运行期为类动态添加一些方法或Field
Target(目标对象)
代理的目标对象
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程
Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
Proxy(代理)
一个类被AOP织入增强后,就产生一个结果代理类
Aspect(切面)
是切点和通知(引介)的结合
!!!以上都是概念性东西比较抽象,后续会补充XML配置和Java配置!!!
学习Spring的AOP要明确的事
1. 开发阶段(我们要做的)
编写核心业务代码(主线)
把公用的代码抽取出来,制成通知
配置文件中,声明切入点与通知间的关系,既切面
2. 运行阶段(Spring框架完成)
Spring框架监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对想的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成对代码的逻辑运行
实战AOP(基于xml配置)
1.引入pom文件
<dependencies>
<!--使用IOC核心依赖包-->
<denpendency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</denpendency>
<!--解析AOP表达式的依赖包-->
<denpendency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</denpendency>
</dependencies>
2.模拟业务场景
// 模拟类
public interface AccountService(){
// 模拟保存用户
void saveAccount();
// 模拟更新用户
void updateAccount(int i);
// 模拟删除用户
int deleteAccount();
}
!!!上述分别对应 无返回无参 | 无返回有参 | 有返回无参!!!
public class AccountServiceImpl implements AccountService{
@Override
public void saveAccount(){
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i){
System.out.println("执行了更新"+ i);
}
@Override
public int deleteAccount(){
System.out.println("执行了删除");
return 0;
}
}
实现了上述接口模拟业务
public class Logger{
// 打印日志,计划切入点方法之前执行(切入点业务层方法)
public void printLog(){
System.out.println("Logger类中的pringLog方法开始记录了....");
}
}
计划在业务之前执行一次日志打印方法
3.配置bean.xml
引入aop服务描述文件,配置aop标签
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd ">
<!-- 不用多做介绍了... -->
<bean id="accountService" class="com.heartskyhigh.service.impl.AccountServiceImpl"></bean>
<bean id="logger" class="com.heartskyhigh.utils.Logger"></bean>
<!-- 配置Aop配置,文件头引入描述文件可以使用标签
1. 把通知Bean交给Spring管理
2. 使用aop:config标签表明AOP的配置
3. 使用aop:aspect标签表明配置切面
id属性:给切面提供唯一标识
ref:是指定通知类的bean的Id
4. 在aop:aspect标签的内部使用对应标签来配置通知的类型
示例让pringLog方法在切入点之前调用,属于前置通知
aop:before 表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,指的是对业务层中哪些方法增强
切入点表达式
关键字:execution
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
1.标准表达式写法
public void com.heartskyhigh.service.impl.AccoutServiceImpl.saveAccount()
2.访问修饰符可以省略,返回值可以用通配符代表任意返回值
* com.heartskyhigh.service.impl.AccoutServiceImpl.saveAccount()
3.包名可以使用通配符,表示任意包,有几级包,就需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount()
4.包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
5.类名和方法名都可以使用*来通配
* *..*.*() 注意没有声明参数,只有两个方法被调用
6.参数列表(可以直接指定参数列表) (基本数据类型 int)(引用数据类型 java.lang.String)
可以使用通配符表示任意类型,但是必须有参数
* *..*.*(*) 注意只有一个带参数的方法,对此方法进行增强
可以使用.. 表示有无参数均可,有参数为任意参数
* *..*.*(..) 所有方法均执行
7.全通配写法
* *..*.*(..)
8.实际开发情况下通常写法是切到业务层实现类下所有方法
* com.heartskyhigh.service.impl.*.*(..)
-->
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知类型,建立通知方法和切入点关联 -->
<aop:before method="printLog"
pointcut="execution(public void com.heartskyhigh.service.impl.AccoutServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</bean>
!!!也可以替换成这种方式,通用一个切面表达式!!!
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd ">
<!-- 不用多做介绍了... -->
<bean id="accountService" class="com.heartskyhigh.service.impl.AccountServiceImpl"></bean>
<bean id="logger" class="com.heartskyhigh.utils.Logger"></bean>
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知类型,建立通知方法和切入点关联 -->
<aop:pointcut id="pt" expression="execution(* com.heartskyhigh.service.impl.*.*(..))"></aop:pointcut>
<aop:before method="printLog" pointcut-ref="pt"/>
<!--
<aop:after-returning method="afterReturiningPrintLong" pointcut-ref="pt"/>
<aop:after-throwing method="afterThrowingPrintLong" pointcut-ref="pt"/>
<aop:after method="afterPrintLong" pointcut-ref="pt"/>
-->
</aop:aspect>
</aop:config>
</bean>
!!!测试代码!!!
public class AopTest{
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
AccountService as = ac.getBean("accountService",AccountService.class);
as.saveAccount();
}
}
4.单元测试
!!!执行操作!!!
通过上述操作,成功出现了结果!!!
5.环绕通知(around)
原谅我要单独拿出来讲它,它与其他四个很不一样。。。。
!!!不废话,上代码!!!
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知类型,建立通知方法和切入点关联 -->
<aop:pointcut id="pt" expression="execution(* com.heartskyhigh.service.impl.*.*(..))"></aop:pointcut>
<!-- 添加环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
public void aroundPrintLog(){
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。。");
}
public class AopTest{
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
AccountService as = ac.getBean("accountService",AccountService.class);
as.saveAccount();
}
}
让我们看一下结果。。。。
!!!为什么只出现了我要增加的功能,却忽略了我业务层的功能呢。。。。!!!
通过分析动态代理,我们知道动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。。。
Spring框架为我们提供了一个接口:ProceedingJoinPoint 该接口有一个方法proceed(),此方法相当于明确调用切入点方法
该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
修改我们要环绕的功能类
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
// 得到方法需要的参数
Object[] args = pjp.getArgs();
// 写在这里就是——前置通知 proceed()方法之前
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。。");
// 切入点方法,明确调用,而且这个异常时Throwable
rtValue = pjp.proceed(args);
// 写在这里就是——后置通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。。");
return rtValue;
}catch(Throwable t){
// 写在这里就是——异常通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。。");
throw new RuntimeException(t);
}finally{
// 写在这里就是最终通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。。");
}
}
它是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。。。
实战AOP(基于java配置)
不罗嗦。。直接上配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd ">
<!-- 添加组建扫描 -->
<context:component-scan base-package="com.heartskyhigh"></context:component-scan>
<!-- 配置spring-aop注解支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</bean>
上述配置可以继续简化为java配置
@Configuration
@ComponentScan(basePackage="com.heartskyhigh")
@EnableAspectJAutoProxy
public class SpringConfiguration{
}
模拟业务层代码
@Service //添加注解,采用组建扫描方式
public class AccountServiceImpl implements AccountService{
@Override
public void saveAccount(){
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i){
System.out.println("执行了更新"+ i);
}
@Override
public int deleteAccount(){
System.out.println("执行了删除");
return 0;
}
}
@Component("logger")
@Aspect // 表示当前类是个切面类
public class Logger{
// 声明切点
@Pointcut(execution(* com.heartskyhigh.service.impl.*.*(..)))
public void pt(){}
// 打印日志,计划切入点方法之前执行(切入点业务层方法)
//@Before("pt()")
//public void printLog(){
// System.out.println("Logger类中的pringLog方法开始记录了....");
//}
@Around("pt()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
// 得到方法需要的参数
Object[] args = pjp.getArgs();
// 写在这里就是——前置通知 proceed()方法之前
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。前置");
// 切入点方法,明确调用,而且这个异常时Throwable
rtValue = pjp.proceed(args);
// 写在这里就是——后置通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。后置");
return rtValue;
}catch(Throwable t){
// 写在这里就是——异常通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally{
// 写在这里就是最终通知
System.out.println("Logger类中aroundPrintLog方法开始记录日志了。。。最终");
}
}
}
!!!Run一下。。。。。。执行之后成功!!!
总结一下,AOP好处就是在不修改原有的业务逻辑下,额外增加新的功能,目的就是与原有程序解耦,好处大大地。。。。