SpringInAction.4th.面向切面的Spring
@(spring)[AOP]
面向切面编程,按我的理解就是,在执行一个动作的同时执行一些公共的动作。这些公共的动作没必要每个都写在各自的方法里,可以提取出公共的方法。Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
AOP术语
spring AOP有很多伤脑筋的术语,没办法为了理解AOP,还是得稍微看下:
1. 通知(Advice):定义了切面是什么以及何时使用。通俗的说通知就是自己定义的要执行的公共的方法(例如发短信方法前后要执行流水操作方法)
2. 连接点(Join point):我们的应用可能也有数以千计的时机应用通知。这些时机被称为连接点。程序执行过程中能够应用通知的所有点
3. 切点(Poincut):切点就定义了“何处”。通俗的说,切点就是在公共方法执行后要执行的方法(或叫那个操作点例如上面的发短信方法)
4. 切面(Aspect):切面是通知和切点的结合。
5. 引入(Introduction):引入允许我们向现有的类添加新方法或属性。
6. 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。
简单归纳
1.切面 = 通知+切点:是什么东西在什么时候使用在什么地方使用(说白了切面就类似于代理类吧)
2.切面、连接点都是名词
3.引入和织入是动词,是动作
AOP实现
编写切点
切点的编写格式:
AOP简单实例:
1、定义简单的切面
@Aspect
public class Audience {
@Before("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void silence(){
System.out.println("phone silence...");
}
@Before("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void taslSeats(){
System.out.println("task seats.");
}
@AfterReturning("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void applause(){
System.out.println("Clap...");
}
@AfterThrowing("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void demendRefund(){
System.out.println("demend refund");
}
}
2、改进切面@Pointcut
通过@Pointcut注解声明频繁使用的切点表达式,perform 方法本身并不重要,它只是一个标识
@Aspect
public class Audience {
// perform 本身并不重要,它只是一个标识
@Pointcut("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void perform(){}
@Before("perform()")
public void silence(){
System.out.println("phone silence...");
}
@Before("perform()")
public void taslSeats(){
System.out.println("task seats.");
}
@AfterReturning("perform()")
public void applause(){
System.out.println("Clap...");
}
@AfterThrowing("perform()")
public void demendRefund(){
System.out.println("demend refund");
}
}
3、在JavaConfig中启用AspectJ注解的自动代理
如果只是单单的只是作为一个@bean注入就来,那么Spring无法将audience 识别为一个切面,所以使用@EnableAspectJAutoProxy开启自动代理,让audience注册为一个切面{@see AspectJAutoProxyRegistrar.class}。
AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。在这种情况下,将会为{basePackge}下的类创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。
/**
* spring 配置类
*/
@Configuration
@ComponentScan
@EnableAspectJAutoProxy // 启用AspectJ注解的自动代理,让audience注册为一个切面{@see AspectJAutoProxyRegistrar.class}
public class SpringConfig {
@Bean
public Audience audience(){
return new Audience();
}
}
注意:测试类中切点中的方法必须是接口方法(涉及到代理的知识点)
创建环绕通知切面
使用环绕通知重新实现Audience切面
需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用(意思就是只会执行上下文方法,而不会去执行通知方法,有可能就是要这种效果)。
/**
* 定义环绕通知
*/
@Aspect
public class Audience02 {
//切点
@Pointcut("execution(* com.lemontree.spring4.day02.Performance.perform(..))")
public void perform(){}
@Around("perform()")
public void warchperform(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("silencing cell phone");
proceedingJoinPoint.proceed(); //通知
System.out.println("Clap ...");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
}
}
}
处理通知中的参数
在切点表达式中声明参数,这个参数传入到通知方法中
public interface Performance {
void perform();
void needArg(int num);
}
@Aspect
public class TrackAop {
@Pointcut("execution(* com.lemontree.spring4.day02.Performance.needArg(int))"
+ "&&args(num)")
public void needArg(int num) {};
@Before("needArg(num)")
public void count(int num) {
System.out.println("num -> do"+num);
}
}
调用者传入的参数会入到cont里面。
通过注解引入新功能
利用被称为引入的AOP概念,切面可以为Spring bean添加新方法。
引入其实就是:一个接口a,一个实现类c,一个AOP引入,引入里面定义一个接口b(+号的意思就是子类的意思)子类和前面的实现类c,意思就是说,b的子类可以使用c里面的方法(当转换为接口a类型时才可以)。
/**
* @Author: YLBG-YCY-1325
* @Description:
* @Date: 2017/8/21
*/
public interface Encoreable {
void doSomething();
}
public class DefaultEncoreableImpl implements Encoreable{
@Override
public void doSomething() {
System.out.println("do something....");
}
}
@Aspect
public class Encoreableroducer {
@DeclareParents(value = "com.lemontree.spring4.day02.Performance+",
defaultImpl = DefaultEncoreableImpl.class)
public static Encoreable encoreable;
}
这么说可能不好理解,看下测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class Spring4Test02 {
@Autowired
private Performance audiencePerform;
@Test
public void test(){
Encoreable en = (Encoreable) audiencePerform;
en.doSomething();
}
}
在XML中声明切面
有这样一种原则,那就是基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置。但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须转向XML配置了。
在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面:
与java 配置类似,切点可以单独提出来进行配置,<\aop:aspectj-autoproxy/> xml配置默认开启aop,所以不需要这个标签也是可以的
<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 class="com.lemontree.spring3.Aop.AudiencePerform" id="perform"/>
<!--切面1-->
<bean class="com.lemontree.spring3.Aop.Audience" id="audience"/>
<!--切面2-->
<bean class="com.lemontree.spring3.Aop.Audience02" id="audience02"/>
<!--开启aop-->
<aop:aspectj-autoproxy/>
<!--前置、后置-->
<aop:config>
<aop:aspect id="audience" ref="audience">
<!--定义切点-->
<aop:pointcut id="ap" expression="execution(* com.lemontree.spring3.Aop.Performance.perform(..))"/>
<aop:before method="silence" pointcut-ref="ap"/>
<aop:before method="taslSeats" pointcut-ref="ap"/>
<aop:after method="applause" pointcut-ref="ap"/>
<aop:after-throwing method="demendRefund" pointcut-ref="ap"/>
</aop:aspect>
</aop:config>
<!--环绕-->
<aop:config>
<aop:aspect id="audience2" ref="audience02">
<!--定义切点-->
<aop:pointcut id="ap" expression="execution(* com.lemontree.spring3.Aop.Performance.perform(..))"/>
<aop:around method="warchperform" pointcut-ref="ap"/>
</aop:aspect>
</aop:config>
</beans>
来看看切面的定义:
public class Audience {
public void silence(){
System.out.println("phone silence...");
}
public void taslSeats(){
System.out.println("task seats.");
}
public void applause(){
System.out.println("Clap...");
}
public void demendRefund(){
System.out.println("demend refund");
}
}
为通知传递参数
代码演示就去掉了,不过要注意一点,那就是xml中&&会被解析为实体的开始,所以要用and来代替。
通过切面引入新的功能
使用default-impl来直接标识委托和间接使用delegate-ref的区别在于后者是Spring bean,它本身可以被注入、通知或使用其他的Spring配置。(大致和java的配置一样)
<aop:config>
<aop:aspect>
<aop:declare-parents
types-matching="com.lemontree.spring3.Aop.Performance+"
implement-interface="com.lemontree.spring3.Aop.Encoreable"
delegate-ref="defaultEncoreable"/>
<!--default-impl="com.lemontree.spring3.Aop.DefaultEncoreableImpl"/>-->
</aop:aspect>
</aop:config>
测试类:
<aop:config>
<aop:aspect>
<aop:declare-parents
types-matching="com.lemontree.spring3.Aop.Performance+"
implement-interface="com.lemontree.spring3.Aop.Encoreable"
default-impl="com.lemontree.spring3.Aop.DefaultEncoreableImpl"/>
</aop:aspect>
</aop:config>
总结
其实AOP代理本质就是,让别人代你做事情,别人可以在你要做的事情的前后做一些其他的事情。Spring里面就是说,调用者实际调用的不是实际的类(或说是方法),调用者调用的是代理类,代理类方法做完后再去处理实际类的方法。
而引入的本质就是,当你用另外一个接口来声明(或者叫强转)时,你可以用这个接口实现类(引入了的实现类)的方法。