Spring对AOP的支持
-
基于代理的经典AOP;
-
@AspectJ注解驱动的切面;
-
纯POJO切面;
-
注入式AspectJ切面(适合Spring个版本);
Spring是在运行期将切面织入到所管理的Bean中的,如图所示,代理类封装了目标类,当拦截到方法调用时,在调用目标Bean的方法之前,代理会执行切面逻辑。真正应用需要被代理的Bean时,Spring才会创建代理对象。Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。
image
Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。
Spring只支持方法连接点,缺少对字段连接点的支持,例如拦截对象字段的修改。也不支持构造器连接点,也就无法在Bean创建时应用通知。
使用切点选择连接点
Spring AOP中,需要使用AspectJ的切点表达式来定义切点。
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为执行类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation() | 限制匹配带有指定注解连接点 |
编写切点
image
使用AspectJ切点表达式来定位
这里使用了execution()指示器来选择Instrument的play()方法。表达式以*
开头表示不关心返回值的类型,然后指定了全限定类名和方法名,使用..
作为方法的参数列表,表示可以是任意的入参。
使用&&
将execution()和within()进行连接,那么也就可以使用||
(或)和!
(非)。
image
使用within()指示器限制切点范围
使用Spring的bean()指示器
bean()使用Bean id来作为参数,从而限制切点只匹配特定的Bean,如:
execution(* com.springinaction.springidol.Instrument.play()) and bean(eddie)
这里,表示在执行Instrument的play()方法时应用通知,但限定Bean的id为eddie。
在XML中声明切面
AOP配置元素 | 描述 |
---|---|
<aop:advisor> |
定义AOP通知器 |
<aop:after> |
定义AOP后置通知(不管该方法是否执行成功) |
<aop:after-returning> |
在方法成功执行后调用通知 |
<aop:after-throwing> |
在方法抛出异常后调用通知 |
<aop:around> |
定义AOP环绕通知 |
<aop:aspect> |
定义切面 |
<aop:aspect-autoproxy> |
定义@AspectJ 注解驱动的切面 |
<aop:before> |
定义AOP前置通知 |
<aop:config> |
顶层的AOP配置元素,大多数的<aop:*> 包含在<aop:config> 元素内 |
<aop:declare-parent> |
为被通知的对象引入额外的接口,并透明的实现 |
<aop:pointcut> |
定义切点 |
所需jar包:
无标题.png
表演者接口:
public interface Performer {
void perform();
}
歌唱家类:
public class Instrumentalist implements Performer {
private String song;
private Instrument instrument;
public Instrumentalist() {
}
public void perform() {
System.out.print("Playing " + song + " : ");
instrument.play();
}
public void setSong(String song) { // 注入歌曲
this.song = song;
}
public String getSong() {
return song;
}
public String screamSong() {
return song;
}
public void setInstrument(Instrument instrument) { // 注入乐器
this.instrument = instrument;
}
}
乐器接口:
public interface Instrument {
public void play();
}
吉他类:
public class Guitar implements Instrument {
public void play() {
System.out.println("Strum strum strum");
}
}
下面定义一个观众类:
public class Audience { // 表演之前
public void takeSeats() {
System.out.println("The audience is taking their seats.");
} // 表演之前
public void turnOffCellPhones() {
System.out.println("The audience is turning off their cellphones");
} // 表演之后
public void applaud() {
System.out.println("CLAP CLAP CLAP CLAP CLAP");
} // 表演失败之后
public void demandRefund() {
System.out.println("Boo! We want our money back!");
}
}
声明前置和后置通知
<?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="eddie" class="com.springinaction.springidol.Instrumentalist">
<property name="instrument">
<bean class="com.springinaction.springidol.Guitar" />
</property>
<property name="song" value="my love" />
</bean>
<bean id="audience" class="com.springinaction.springidol.Audience" />
<aop:config>
<aop:aspect ref="audience"><!-- 引用audience Bean -->
<!-- 声明切入点 -->
<aop:pointcut id="performance"
expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
<!-- 表演之前 -->
<aop:before pointcut-ref="performance" method="takeSeats" />
<aop:before pointcut-ref="performance" method="turnOffCellPhones" />
<!-- 表演之后 -->
<aop:after-returning pointcut-ref="performance"
method="applaud" />
<!-- 表演失败之后 -->
<aop:after-throwing pointcut-ref="performance"
method="demandRefund" />
</aop:aspect>
</aop:config></beans>
在<aop:config>
中,可以声明一个或多个通知器、切面或者切点。pointcut
属性定义了通知所引用的切点。最终的通知逻辑如何织入到业务逻辑中:
image
Audience切面包含4中通知,这些通知把通知=逻辑织入到匹配的切面的切点方法中
测试代码:
@Test
public void testBeforeAndAfter() throws PerformanceException{
ApplicationContext context = new ClassPathXmlApplicationContext("spring-idol.xml");
Performer performer = (Performer) context.getBean("eddie");
performer.perform();
}
测试结果:
The audience is taking their seats.
The audience is turning off their cellphones
Playing my love : Guitar Guitar Guitar
CLAP CLAP CLAP CLAP CLAP
声明环绕通知
前置通知和后置通知之间共享消息需要使用成员变量,而Audience是单例,使用成员变量有可能存在线程安全问题。使用环绕通知可以完成之前前置和后置所实现的相同功能,而且只需一个方法。
package com.springinaction.springidol;
import org.aspectj.lang.ProceedingJoinPoint;
public class AroundAudience {
public void watchPerformance(ProceedingJoinPoint joinpoint) {
try { // 表演之前
System.out.println("The audience is taking their seats.");
System.out.println("The audience is turning off their cellphones");
long start = System.currentTimeMillis(); // 执行被通知的方法
joinpoint.proceed(); // 表演之后
long end = System.currentTimeMillis();
System.out.println("CLAP CLAP CLAP CLAP CLAP");
System.out.println("The performance took " + (end - start) + " milliseconds.");
} catch (Throwable t) { // 表演失败之后
System.out.println("Boo! We want our money back!");
}
}
}
ProceedingJoinPoint
作为入参,从而可以在通知里调用被通知的方法。
XML配置:
<bean id="audience" class="com.springinaction.springidol.AroundAudience" />
<aop:config>
<aop:aspect ref="audience"><!-- 引用audience Bean -->
<!-- 声明切入点 -->
<aop:pointcut id="performance"
expression="execution(* com.springinaction.springidol.Performer.perform(..))" />
<aop:around method="watchPerformance" pointcut-ref="performance" />
</aop:aspect>
</aop:config></pre>
一个实际例子---日志记录
AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1)JoinPoint
java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
Signature getSignature() :获取连接点的方法签名对象;
java.lang.Object getTarget() :获取连接点所在的目标对象;
java.lang.Object getThis() :获取代理对象本身;
2)ProceedingJoinPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了个用于执行连接点方法的方法:
java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
二、代码演示。
SecurityHandler.java
package com.tgb.spring;
import org.aspectj.lang.JoinPoint;
public class SecurityHandler{
private void checkSecurity(JoinPoint joinPoint){
for (int i = 0; i < joinPoint.getArgs().length; i++) {
System.out.println(joinPoint.getArgs()[i]);
}
System.out.println(joinPoint.getSignature().getName());
System.out.println("=====checkSecurity====");
}
}
Client.java
package com.tgb.spring;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.tgb.spring.UserManager;
public class Client {
public static void main(String[] args) {
BeanFactory factory=new ClassPathXmlApplicationContext("applicationContext.xml");
UserManager userManager=(UserManager) factory.getBean("userManager");
userManager.addUser("张三", "123");
//userManager.delUser(1);
}
}
UserManager.java
package com.tgb.spring;
public interface UserManager {
public void addUser(String username,String password);
public void delUser(int userId);
public String findUserById(int userId);
public void modifyUser(int userId,String username,String password);
}
UserManagerImpl.java
package com.tgb.spring;
public class UserManagerImpl implements UserManager {
public void addUser(String username, String password) {
//checkSecurity();
System.out.println("===UserManager.addUser===");
}
public void delUser(int userId) {
//checkSecurity();
System.out.println("===UserManager.delUser===");
}
public String findUserById(int userId) {
//checkSecurity();
System.out.println("===UserManager.findUserById===");
return "张三";
}
public void modifyUser(int userId, String username, String password) {
//checkSecurity();
System.out.println("===UserManager.modifyUser===");
}
// private void checkSecurity(){
// System.out.println("checkSecurity");
//
// }
}
applicationContext.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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<bean id="userManager" class="com.tgb.spring.UserManagerImpl" />
<bean id="securityHandler" class="com.tgb.spring.SecurityHandler"/>
<aop:config>
<aop:aspect id="securityAspect" ref="securityHandler">
<aop:pointcut id="addAddMethod" expression="execution(* com.tgb.spring.*.*(..))" />
<aop:before method="checkSecurity" pointcut-ref="addAddMethod" />
</aop:aspect>
</aop:config>
</beans>