Spring 学习3
AOP(面向切面编程)
利用 AOP 可以对业务逻辑的各部分进行隔离,使业务逻辑的各部分耦合度降低,提高程序可重用性。
简单理解就是把我们程序重复的代码抽取出来,在需要执行的时候,用动态代理的技术,在不改源码的基础上,对我们的已有方法进行增强。
名词解释
Joinpoint(连接点):指被拦截的点,在 Spring 中这些点指方法, Spring 只支持方法类型的连接点。业务方法和增强代码通过连接点联系在一起。
Pointcut(切入点):指我们要对哪些 Joinpoint 进行拦截的定义。 连接点被增强了,它就变成切入点
提示:所有切入点都是连接点,但是,所有连接点不都是切入点。
Advice(翻译–建议,理解为增强可能好一点):指拦截到 Joinpoint 之后所要做的事情就是建议。 这里建议分为:前置建议、后置建议、异常建议、最终建议、环绕建议。
Introduction(引进): 引进是一种特殊的建议在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field
Target(目标): 代理的目标对象。
Weaving(织入): 是指把建议应用到目标对象来创建新的代理对象的过程。 Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
Proxy(代理): 一个类被 AOP 织入建议后,就产生一个结果代理类。
Aspect(切面): 是切入点和建议(引进)的结合。
开发写代码时,可以先完成核心业务代码,再把公共代码抽取出来制作成建议,在配置文件中声明切入点和建议的联系。Spring 会监控切入点方法的执行,一旦监控到切入点方法要执行,就用代理机制,动态创建目标对象的代理对象,根据建议的类型会在代理对象相应的位置将建议的功能织入,组成完整代码并运行。
在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理(实现接口或继承子类)的方式。
创建演示项目(Maven 方式创建)
pom.xml 文件里添加2个依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.rgb3</groupId>
<artifactId>noteProject</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
</project>
aspectjweaver 是因为用到切入点表达式。
AOP 配置介绍(基于 XML )
<aop:config>:开始 AOP 配置。
<aop:aspect>:配置切面。
属性 id:给切面一个唯一标识。 ref:指定建议(增强)类 bean 的 id。
<aop:before>:表示前置建议。
属性 method:指定建议(增强)类中的哪个方法作为前置建议。
pointcut:指定切入点表达式,表达式的含义是指定对哪一个类的哪一个方法增强。
切入点表达式写法:
execution(访问修饰符 返回值 包名.包名…类名.方法名(参数))
如:execution(public void com.rgb3.service.impl.AccountServiceImpl.saveAccount())
提示:
访问修饰符可以省略
execution(void com.rgb3.service.impl.AccountServiceImpl.saveAccount())
返回值可以用通配符*,表示任意返回值
execution(* com.rgb3.service.impl.AccountServiceImpl.saveAccount())
包名可以用通配符*表示任意包,每一级包用1个*。也可以用…表示任意包及其子包
execution(* *.*.*.impl.AccountServiceImpl.saveAccount())
execution(* *…impl.AccountServiceImpl.saveAccount())
类名和方法名都可以用通配符* 来表示
execution(* *…*.*())
方法参数
基本类型可以直接写,如 int
引用类型写包名.类名,如 java.lang.String
可以直接用…表示有无参数都行,即使有参数也可以是任意类型
可以用通配符*表示任意类型,但必须有参数
全通配写法(一般不用):execution(* *…*.*(…))
实际中常用写法
execution(* com.rgb3.service.impl.*.*(…))
<aop:after-returning> 后置建议,属性同上
<aop:after-throwing> 异常建议,属性同上
<aop:after> 最终建议,属性同上
演示例子:
IAccountService 接口
public interface IAccountService {
void saveAccount();//模拟保存账户
}
AccountServiceImpl 类,实现 IAccountService 接口
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("保存用户");
//int i=1/0;
}
}
工具类 AppLogger ,通过打印语句演示作用
//工具类
public class AppLogger {
//模拟前置打印日志
public void beforePrintLog(){
System.out.println("AppLogger工具类的beforePrintLog执行----前置");
}
//模拟后置打印日志
public void afterPrintLog(){
System.out.println("AppLogger工具类的afterPrintLog执行---后置");
}
//模拟出现异常打印日志
public void throwingPrintLog(){
System.out.println("AppLogger工具类的throwingPrintLog执行---异常");
}
//模拟最终打印日志
public void finalPrintLog(){
System.out.println("AppLogger工具类的finalPrintLog执行---最终");
}
}
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"
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">
<bean id="accountService" class="com.rgb3.service.impl.AccountServiceImpl"></bean>
<bean id="appLogger" class="com.rgb3.util.AppLogger"></bean>
<!--配置AOP-->
<aop:config>
<aop:aspect id="logAdvice" ref="appLogger">
<aop:before method="beforePrintLog"
pointcut="execution(* com.rgb3.service.impl.*.*(..))"></aop:before>
<aop:after-returning method="afterPrintLog"
pointcut="execution(* com.rgb3.service.impl.*.*(..))">
</aop:after-returning>
<aop:after-throwing method="throwingPrintLog"
pointcut="execution(* com.rgb3.service.impl.*.*(..))">
</aop:after-throwing>
<aop:after method="finalPrintLog"
pointcut="execution(* com.rgb3.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
启动类 AppNoteDemo
public class AppNoteDemo {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService=(IAccountService)ac.getBean("accountService");
//打印保存用户语句
accountService.saveAccount();
}
}
在 AccountServiceImpl 类里的 saveAccount()方法里添加一个错误让他出现异常
public void saveAccount() {
System.out.println("保存用户");
int i=1/0;
}
配置 AOP 语句优化
切入点表达式可以单独写出来,用<aop:pointcut>
<aop:pointcut> 标签可以写在<aop:aspect>标签内,表示这个表达式只能在当前的切面里使用。
写在<aop:aspect>标签外,表示所有切面都可以用这个表达式,注意,此时表达式要写在切面的前面。
属性:
id:指定表达式的唯一标识
expression:表达式内容
写在<aop:aspect>标签内
<aop:config>
<aop:aspect id="logAdvice" ref="appLogger">
<aop:before method="beforePrintLog" pointcut-ref="pointCut1"></aop:before>
<aop:after-returning method="afterPrintLog" pointcut-ref="pointCut1">
</aop:after-returning>
<aop:after-throwing method="throwingPrintLog" pointcut-ref="pointCut1">
</aop:after-throwing>
<aop:after method="finalPrintLog" pointcut-ref="pointCut1"></aop:after>
<!--切入点表达式单独写出来-->
<aop:pointcut id="pointCut1"
expression="execution(* com.rgb3.service.impl.*.*(..))">
</aop:pointcut>
</aop:aspect>
</aop:config>
写在<aop:aspect>标签外,此时表达式要写在切面的前面。
<aop:config>
<!--切入点表达式单独写在前面-->
<aop:pointcut id="pointCut1"
expression="execution(* com.rgb3.service.impl.*.*(..))">
</aop:pointcut>
<aop:aspect id="logAdvice" ref="appLogger">
<aop:before method="beforePrintLog" pointcut-ref="pointCut1"></aop:before>
<aop:after-returning method="afterPrintLog" pointcut-ref="pointCut1">
</aop:after-returning>
<aop:after-throwing method="throwingPrintLog" pointcut-ref="pointCut1">
</aop:after-throwing>
<aop:after method="finalPrintLog" pointcut-ref="pointCut1"></aop:after>
</aop:aspect>
</aop:config>
<aop:around> 环绕通知
属性与 before 、after 他们一样
Spring 提供了一个接口 ProceedingJoinPoint ,这个接口有一个方法 proceed() ,这个方法用于调用切入点方法。这个接口可以作为环绕通知的方法参数,在程序执行时, Spring 会提供这个接口的实现类给我们用。
AOP 配置改为:
<!--配置AOP-->
<aop:config>
<!--切入点表达式单独写在前面-->
<aop:pointcut id="pointCut1" expression="execution(* com.rgb3.service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="logAdvice" ref="appLogger">
<aop:around method="aroundPrintLog" pointcut-ref="pointCut1"></aop:around>
</aop:aspect>
</aop:config>
AppLogger 类中增加一个方法:
//模拟打印环绕日志
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object returnValue=null;
Object[] args=proceedingJoinPoint.getArgs(); //得到切入点方法执行所需要的参数
try {
System.out.println("AppLogger工具类的aroundPrintLog执行---前置");//前置建议
returnValue=proceedingJoinPoint.proceed(args); //明确调用切入点方法
System.out.println("AppLogger工具类的aroundPrintLog执行---后置");//后置建议
return returnValue;
}catch (Throwable t){
System.out.println("AppLogger工具类的aroundPrintLog执行---异常");//异常建议
throw new RuntimeException(t);
}finally {
System.out.println("AppLogger工具类的aroundPrintLog执行---最终");//最终建议
}
}
returnValue=proceedingJoinPoint.proceed(args) 明确调用切入点方法
- 在这句之前的代码,就是前置建议,像 XML 配置里的<aop:before> 标签
- 在这句之后的代码,就是后置建议,像 XML 配置里的<aop:after-returning> 标签
- 在 catch 块里的,就是异常建议,像 XML 配置里的<aop:after-throwing> 标签
- 在 finally 块里的,就是最终建议,像 XML 配置里的<aop:after> 标签
观察一下代码,感觉像是把 XML 配置里的代码,转移到 java 代码里,让我们在代码中手动控制建议方法什么时候执行。
AOP 配置(基于注解)
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">
<!--Spring 创建容器时要扫描的包-->
<context:component-scan base-package="com.rgb3"></context:component-scan>
<!--Spring 开启注解 AOP 支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
AccountServiceImpl 类上添加 @Service(“accountService”) 注解,里面的代码与上面的例子一样
-
@Aspect: 写在类上,表示这个类是切面类
-
@Pointcut:参数写切入点表达式。这个注解写在方法上,方法名可作为引用这个切入点表达式的名字。
//切入点表达式 @Pointcut("execution(* com.rgb3.service.impl.*.*(..))") private void pointCut1(){}
-
@Before:前置建议,使用:
@Before("pointCut1()") public void beforePrintLog(){ System.out.println("AppLogger工具类的beforePrintLog执行----前置"); }
-
@AfterReturning:后置建议,使用同上
-
@AfterThrowing:异常建议,使用同上
-
@After:最终建议,使用同上
-
@Around:环绕建议,使用同上
AppLogger 工具类
@Component("appLogger")
@Aspect //表示这是个切面类
public class AppLogger {
//切入点表达式
@Pointcut("execution(* com.rgb3.service.impl.*.*(..))")
private void pointCut1(){}
//模拟前置打印日志
@Before("pointCut1()")
public void beforePrintLog(){
System.out.println("AppLogger工具类的beforePrintLog执行----前置");
}
//模拟后置打印日志
@AfterReturning("pointCut1()")
public void afterPrintLog(){
System.out.println("AppLogger工具类的afterPrintLog执行---后置");
}
//模拟出现异常打印日志
@AfterThrowing("pointCut1()")
public void throwingPrintLog(){
System.out.println("AppLogger工具类的throwingPrintLog执行---异常");
}
//模拟最终打印日志
@After("pointCut1()")
public void finalPrintLog(){
System.out.println("AppLogger工具类的finalPrintLog执行---最终");
}
//模拟打印环绕日志
//@Around("pointCut1()")
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object returnValue=null;
Object[] args=proceedingJoinPoint.getArgs(); //得到切入点方法执行所需要的参数
try {
System.out.println("AppLogger工具类的aroundPrintLog执行---前置");//前置建议
returnValue=proceedingJoinPoint.proceed(args); //明确调用切入点方法
System.out.println("AppLogger工具类的aroundPrintLog执行---后置");//后置建议
return returnValue;
}catch (Throwable t){
System.out.println("AppLogger工具类的aroundPrintLog执行---异常");//异常建议
throw new RuntimeException(t);
}finally {
System.out.println("AppLogger工具类的aroundPrintLog执行---最终");//最终建议
}
}
}
这里先注释了@Around 环绕建议,演示其他注解的效果
这里发现最终建议和后置建议的顺序不对,百度了下这是 Spring自身的原因,所以开发时注意是否要用注解方式。
如果把 @Before、@AfterReturning、@AfterThrowing、@After 都注释了,使用@Around 注解,顺序就是对的