spring5入门与实践第一讲Spring 的AOP
spring的aop是spring的另外一个基本功能,面向切面的编程可以为开发人员提供极大的便利,面向切面编程主要是针对一些控制性功能,如:权限控制,缓存处理,日志控制,事务管理等,这些功能和主要的业务逻辑没有关系,但是却需要整合在很多业务逻辑模块中。
模拟面向切面编程
就此我不希望一开始就讲解很多深奥的aop概念,下面我们先通过一个具体的实例来讨论面向切面的需求和意义,现在我们希望写一个程序来处理菲布尼奇数列,菲布尼奇数列用来获取n = n1+n2的数列,如:n1=1,n2=2,n3=3(1+2),n4=5(2+3),这种方式可以通过两种方式来实现,普通的代码和递归。
首先创建一个菲布尼奇数列的接口,里面有两个方法可以完成菲布尼奇数据的求值。
public interface ICalFibonacci {
long calFabonacciByLoop(long n);
long calFabonacciByRecusion(long n);
}
//具体的实现类
public class CalFibonacciImpl implements ICalFibonacci {
public long calFabonacciByLoop(long n) {
long n1 = 1;
long n2 = 2;
long n3 = 0;
for(int i=3;i<=n;i++) {
n3 = n1+n2;
n1 = n2;
n2 = n3;
}
return n3;
}
public long calFabonacciByRecusion(long n) {
if(n==1) {
return 1;
}
if(n==2) {
return 2;
}
return calFabonacciByLoop(n-2)+calFabonacciByLoop(n-1);
}
}
下面我们希望统计这两种方案的效率,最基本的方式就是在这两个方法执行前加入一个计时代码,在执行完毕之后加入一个计时信息。 所以在接口中增加一个方法专门来统计这两个的执行效率。
public long stat(int method, long n) {
long start = new Date().getTime();
if(method==0) {
calFabonacciByLoop(n);
}
if(method==1) {
calFabonacciByRecusion(n);
}
long end = new Date().getTime();
return end-start;
}
这种方式可以执行统计,但是首先第一个问题:如果将来没有源代码,此时就无法在接口和实现类中添加这个stat方法,这个问题可以通过静态代理模式来解决。
public class CalFibonacciProxy implements ICalFibonacci {
ICalFibonacci icf;
public CalFibonacciProxy(ICalFibonacci icf) {
this.icf = icf;
}
public long calFabonacciByLoop(long n) {
long start = new Date().getTime();
long result = icf.calFabonacciByLoop(n);
long end = new Date().getTime();
System.out.println("Loop:"+(end-start)/1000+"秒");
return result;
}
public long calFabonacciByRecusion(long n) {
long start = new Date().getTime();
long result = icf.calFabonacciByRecusion(n);
long end = new Date().getTime();
System.out.println("Loop:"+(end-start)/1000+"秒");
return result;
}
}
调用的代码
public class TestFibonacci {
public static void main(String[] args) {
long n = 10000;
ICalFibonacci icf = new CalFibonacciProxy(new CalFibonacciImpl());
long n1 = icf.calFabonacciByLoop(n);
long n2 = icf.calFabonacciByRecusion(n);
System.out.println(n1+","+n2);
}
}
以上代码可以完成效率的统计,而且不会对原有代码有任何影响,但是这种所带来的问题就是所有的需要涉及到这个功能的模块都得添加代理类,这样代理类太多,使得代码的维护有难度,下面我们利用java的反射机制来实现动态代理。
首先创建一个Annotation来指定哪些方法上需要添加效率检查。
@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface PerformCheck {
}
在接口方法上添加该Annotation,指定哪些方法需要添加功能
public interface ICalFibonacci {
@PerformCheck
long calFabonacciByLoop(long n);
@PerformCheck
long calFabonacciByRecusion(long n);
}
下一步需要创建该功能的代理对象,这个对象需要实现InvocationHandler接口
public class PerformProxy implements InvocationHandler {
private Object target;
private PerformProxy() {}
//根据传入的对象获取该对象的代理对象
public static Object getInstance(Object o) {
PerformProxy p = new PerformProxy();
p.target = o;
Object r = Proxy.newProxyInstance(o.getClass().getClassLoader(),
o.getClass().getInterfaces(), p);
return r;
}
//可以通过invoke方法来完成代理对象的函数调用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
此时PerformProxy就是一个代理对象,该对象可以代理任何一个对象,主要是通过getInstance方法来实现的,该方法传入了一个Object的对象作为参数,之后通过Proxy的静态方法newProxyInstance方法来创建这个代理对象。任何一个代理对象执行方法时都会执行invoke方法,在该方法中就可以非常方便的为代码增加功能性代码。
//可以通过invoke方法来完成代理对象的函数调用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rel = null;
//如果方法上存在PerformCheck的Annotation
if(method.isAnnotationPresent(PerformCheck.class)) {
long start = System.currentTimeMillis();
rel = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("执行"+method.getName()+"花了:"+(end-start)+"毫秒");
} else {
rel = method.invoke(target, args);
}
return rel;
}
下面我们通过这个代理对象来完成这个操作。
public class TestFibonacci {
public static void main(String[] args) {
long n = 100000;
ICalFibonacci icf = (ICalFibonacci)PerformProxy
.getInstance(new CalFibonacciImpl());
System.out.println(icf.calFabonacciByLoop(n));
System.out.println(icf.calFabonacciByRecusion(n));
}
}
通过这个方法我们就解决了效率统计的问题,而且又不会破坏原来的代码,如果不想用Annotation,可以直接将需要添加这个功能的类和方法添加到配置文件中即可,该方法就不再进行演示了。spring的aop的思路和本小结中演示的动态代理的思路基本类似,下面我们来看一下spring的aop是如何处理的。
spring中AOP的各种术语
spring提供了两种方式来实现动态代理,一种是基于配置文件的,第二种是基于Annotation的,在具体演示之前我们需要了解AOP中的各种术语。
- Advice(通知)
- Pointcut(切点)
- Aspect(切面)
Advice用来表示自定义功能模块在什么时候执行,有如下几种通知:Before(在目标方法执行之前执行),After(在目标方法执行之后执行),After-returning(在目标方法返回并且执行成功之后执行),After-throwing(在目标方法执行完成并且抛出异常时执行),Around(在目标方法调用之前和之后执行);Pointcut表示在那个执行目标上执行该功能,对比上一节的实例就是具体的calFabonacciByLoop和calFabonacciByRecusion两个方法。spring提供了自己的表达式语句来设定Pointcut,Aspect是Advice和Pointcut的集合,他们一起定义了在哪些方法上什么时候执行什么样的功能。这些概念比较抽象,我们通过一个实例来把这些概念搞清楚。
基于xml来实现spring中的AOP
首先需要在maven中增加spring的aop的模块,
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
接着把功能性的横切面创建好
public class PerformAspect {
//使用Before的Advice
public void testBefore() {
System.out.println("run before");
}
//使用After的Advice
public void testAfter() {
System.out.println("run after");
}
//效率统计,使用环绕通知
public long statPerform(ProceedingJoinPoint jp) {
try {
long start = System.currentTimeMillis();
long result = (Long)jp.proceed();
long end = System.currentTimeMillis();
System.out.println(jp.getSignature().getName()+"花费了:"+(end-start));
return result;
} catch (Throwable e) {
e.printStackTrace();
}
return 0;
}
}
下一步需要在xml中加入aop的schema
<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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
然后在beans.xml中创建相应的bean
<bean id="calFibonacci" class="org.konghao.aop.xml.CalFibonacciImpl"/>
<bean id="performAspectRef" class="org.konghao.aop.xml.PerformAspect"/>
下一步设置切面Aspect
<aop:aspect ref="performAspectRef"></aop:aspect>
然后配置切点(pointcut)和通知(advice)
<aop:config>
<aop:aspect ref="performAspectRef">
<aop:pointcut expression="execution(* org.konghao.aop.xml.CalFibonacciImpl.*(...))" id="performPointcut"/>
<aop:before method="testBefore" pointcut-ref="performPointcut"/>
<aop:after method="testAfter" pointcut-ref="performPointcut"/>
<aop:around method="statPerform" pointcut-ref="performPointcut"/>
</aop:aspect>
</aop:config>
完整的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: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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="calFibonacci" class="org.konghao.aop.xml.CalFibonacciImpl"/>
<bean id="performAspectRef" class="org.konghao.aop.xml.PerformAspect"/>
<aop:config>
<aop:aspect ref="performAspectRef">
<aop:pointcut expression="execution(* org.konghao.aop.xml.CalFibonacciImpl.*(...))" id="performPointcut"/>
<aop:before method="testBefore" pointcut-ref="performPointcut"/>
<aop:after method="testAfter" pointcut-ref="performPointcut"/>
<aop:around method="statPerform" pointcut-ref="performPointcut"/>
</aop:aspect>
</aop:config>
</beans>
此时编写代码完成调用
public class Test {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
ICalFibonacci icf = (ICalFibonacci)ctx.getBean("calFibonacci");
long n = 220000;
icf.calFabonacciByLoop(n);
icf.calFabonacciByRecusion(n);
ctx.close();
}
}
这就是基于xml的配置,个人认为这种方式非常的清晰合理,接下来我们基于java配置的方式来处理。
基于java配置的方式来实现aop
基于java配置的方式和基于xml的有一些差别,但也是非常常用的,首先创建一个Config的java文件
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("org.konghao.aop.java")
public class AopConfig {
}
下一步创建切面类和通知
@Aspect
@Component
public class PerformAspect {
//使用Before的Advice
@Before("execution(org.konghao.aop.java.CalFibonacciImpl.*(..)")
public void testBefore() {
System.out.println("run before");
}
//使用After的Advice
@After("execution(org.konghao.aop.java.CalFibonacciImpl.*(..)")
public void testAfter() {
System.out.println("run after");
}
//效率统计,使用环绕通知
@Around("execution(org.konghao.aop.java.CalFibonacciImpl.*(..)")
public long statPerform(ProceedingJoinPoint jp) {
try {
long start = System.currentTimeMillis();
long result = (Long)jp.proceed();
long end = System.currentTimeMillis();
System.out.println(jp.getSignature().getName()+"花费了:"+(end-start));
return result;
} catch (Throwable e) {
e.printStackTrace();
}
return 0;
}
}
非常的简单直接,接下来看看调用的代码
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopConfig.class);
ICalFibonacci icf = (ICalFibonacci)ctx.getBean("calFibonacci");
long n = 220000;
icf.calFabonacciByLoop(n);
icf.calFabonacciByRecusion(n);
ctx.close();
}
}
基于Annotation的方式也非常的直观,但是此处有一个问题,就是在Aspect中,我们把Pointcut和advice写在一起了,这样导致Pointcut变得非常的多,所以感觉非常的繁杂,所以我们可以在Aspect中将Pointcut分开写
@Aspect
@Component
public class PerformAspect {
@Pointcut("execution(* org.konghao.aop.java.CalFibonacciImpl.*(..))")
public void performAspect() {}
//使用Before的Advice
@Before("performAspect()")
public void testBefore() {}
//使用After的Advice
@After("performAspect()")
public void testAfter() {}
//效率统计,使用环绕通知
@Around("performAspect()")
public long statPerform(ProceedingJoinPoint jp) {}
}
在使用Annotation的方式中,可以Pointcut处理具体方法的参数,要处理参数可以通过args传入,参数类型和数量必须和具体的方法完全一致
@Pointcut("execution(* org.konghao.aop.java.CalFibonacciImpl.calFabonacciByLoop(long))&&args(argNum)")
public void testArg(long argNum) {
}
@Before("testArg(argNum)")
public void testArgBefore(long argNum) {
System.out.println("参数测试:"+argNum);
}
以上方法就处理了calFabonacciByLoop方法的参数。
使用spring的aop动态增加方法
spring4之后的版本中可以使用aop动态增加方法,创建一个需要嵌入到某个具体方法中的接口
public interface AdditionAble {
void check();
}
public class AdditionImpl implements AdditionAble {
public void check() {
System.out.println("额外的方法!!!");
}
}
编写相应的Aspect
@Aspect
@Component
public class AdditionAspect {
@DeclareParents(value="org.konghao.aop.java.CalFibonacciImpl",defaultImpl=AdditionImpl.class)
public static AdditionAble additionImpl;
}
此处没有使用Poincut和Advice,我们的需求是在目标方法中嵌入一个新的方法,所以使用DeclareParents来进行声明,在哪个类中添加处理。添加完这个操作之后,AdditionAble就等于CalFibonacciImpl的一个代理对象,可以直接强制类型转换
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopConfig.class);
AdditionAble addition = (AdditionAble)icf;
addition.check();
ctx.close();
}
}
这一部分完整的讲解了Spring的AOP存在的意义和原理,希望对大家有所帮助。