需求:假如,一个业务类有四个方法(加减),我们要在每个方法都加上日志操作。
1、没有AOP,实现日志
1.1 定义接口
定义一个数字运算的接口,其中有两个方法:加法运算和减法运算:
package com.spring.aop;
public interface MathCalculator {
public int add(int a,int b);
public int sub(int a,int b);
}
1.2 定义实现类
定义实现类,除了进行运算,还要打印简单的日志功能:
package com.spring.aop;
public class NonAopMathCalculator implements MathCalculator {
@Override
public int add(int a, int b) {
System.out.println("方法执行前参数是:"+a+","+b);
int result = a+b;
System.out.println("方法执行后结果是:"+result);
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("方法执行前参数是:"+a+","+b);
int result = a-b;
System.out.println("方法执行后结果是:"+result);
return result;
}
}
1.3 测试
package com.spring.aop;
public class MathCalculatorTest {
public static void main(String[] args) {
MathCalculator mathCalculator = new NonAopMathCalculator();
int add = mathCalculator.add(9, 6);
System.out.println("加法计算结果:"+add);
int sub = mathCalculator.sub(9, 6);
System.out.println("减法计算结果:"+sub);
}
}
打印结果:
方法执行前参数是:9,6
方法执行后结果是:15
加法计算结果:15
方法执行前参数是:9,6
方法执行后结果是:3
减法计算结果:3
1.4 存在的问题
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
代码分散:以日志需求为例,只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码, 如果日志需求发生变化, 必须修改所有模块。
2、通过动态代理实现日志
代理设计模式的原理: 使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理, 代理对象决定是否以及何时将方法调用转到原始对象上。这种方式可以解决上述存在的问题:
接口还是上面的,不用改变,现在我们使用动态代理的方式来实现,代码如下:
2.1 创建接口实现
创建接口的实现类,实现类中只做业务相关的事情,日志的事情不需要管:
package com.spring.aop;
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2.2 创建代理类
创代理类,代理类持有被代理对象的引用:
package com.spring.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class MathCalculatorProxyImpl {
//持有被代理对象的引用
private MathCalculatorImpl target;
//通过构造方法传入被代理对象
public MathCalculatorProxyImpl(MathCalculatorImpl target) {
this.target = target;
}
//返回代理对象
public MathCalculator getLoggingProxy(){
MathCalculator proxy = null;
ClassLoader classLoader = target.getClass().getClassLoader();//类加载器
Class[] interfaces = new Class[]{MathCalculator.class};//被代理对象实现的接口的数组
//处理器对象,进行日志操作,被代理对象的所有方法执行都要先经过该类的invoke方法
InvocationHandler handler = new InvocationHandler() {
/**
* proxy: 代理对象。 一般不使用该对象
* method: 正在被调用的方法
* args: 调用方法传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//打印日志
System.out.println("[before] The method " + methodName + " begins with " + Arrays.asList(args));
//调用目标方法
Object result = null;//目录方法返回值
try {
//前置通知
result = method.invoke(target, args);
//返回通知, 可以访问到方法的返回值
} catch (NullPointerException e) {
e.printStackTrace();
//异常通知, 可以访问到方法出现的异常
}
//后置通知. 因为方法可以能会出异常, 所以访问不到方法的返回值
//打印日志
System.out.println("[after] The method ends with " + result);
return result;
}
};
/**
* classLoader: 代理对象使用的类加载器。
* interfaces: 指定代理对象的类型. 即代理代理对象中可以有哪些方法.
* handler: 当具体调用代理对象的方法时, 应该如何进行响应, 实际上就是调用 InvocationHandler 的 invoke 方法
*/
proxy = (MathCalculator) Proxy.newProxyInstance(classLoader,interfaces,handler);
return proxy;
}
}
2.3 测试
package com.spring.aop;
public class MathCalculatorProxyTest {
public static void main(String[] args) {
//创建被代理对象
MathCalculatorImpl mathCalculatorImpl = new MathCalculatorImpl();
//创建代理对象
MathCalculatorProxyImpl mathCalculatorProxy = new MathCalculatorProxyImpl(mathCalculatorImpl);
MathCalculator proxy = mathCalculatorProxy.getLoggingProxy();
int add = proxy.add(9, 6);
System.out.println("加法计算结果:"+add);
int sub = proxy.sub(9, 6);
System.out.println("减法计算结果:"+sub);
}
}
打印结果:
[before] The method add begins with [9, 6]
[after] The method ends with 15
加法计算结果:15
[before] The method sub begins with [9, 6]
[after] The method ends with 3
减法计算结果:3
2.4 总结
动态代理的方式可以实现日志操作,这样实现起来很统一,就算目标类中加了其他方法,不需要任何操作,就能实现日志功能。但是如果每次这样手动实现,也会比较麻烦,所以Spring给我们提供了AOP模块。
3、AOP
3.1 AOP简介
–业务模块更简洁,只包含核心业务代码。
3.2 AOP术语
3.3 AOP的实现AspectJ使用
3.3.1 注解配置使用AspectJ实现AOP
首先要把jar包导入到工程中
1、创建接口和实现类,并在实现类加上Spring注解
package com.spring.aspectjAop.annotation;
public interface MathCalculator {
public int add(int a, int b);
public int sub(int a, int b);
}
package com.spring.aspectjAop.annotation;
import org.springframework.stereotype.Component;
@Component("mathCalculator")
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2、创建AspectJ代理类
package com.spring.aspectjAop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component//首先切面也是spring的一个Bean
@Aspect//表明这是一个AspectJ切面
public class LoggingAspectJAop {
/**
* 标识这个方法是个前置通知, 切点表达式表示执行任意类的任意方法. 第
* 一个 * 代表匹配任意修饰符及任意返回值, 第二个 * 代表任意类的对象,
* 第三个 * 代表任意方法, 参数列表中的 .. 匹配任意数量的参数
*/
@Before("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("执行的方法是:"+methodName+",参数是:"+args);
}
}
3、配置文件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: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-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置包扫描-->
<context:component-scan base-package="com.spring.aspectjAop.annotation"/>
<!--开启AspectJ的注解-->
<aop:aspectj-autoproxy/>
</beans>
4、测试
package com.spring.aspectjAop.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
MathCalculator mathCalculator = (MathCalculator) applicationContext.getBean("mathCalculator");
int add = mathCalculator.add(9, 3);
System.out.println("加法结果:"+add);
int sub = mathCalculator.sub(9, 3);
System.out.println("减法结果:"+sub);
}
}
打印结果:
执行的方法是:add,参数是:[9, 3]
加法结果:12
执行的方法是:sub,参数是:[9, 3]
减法结果:6
4、总结
1、要使用AspectJ,就要先导入相关的jar包;
2、applicationContext.xml文件中,要加入aop名称空间,启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>;Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理。
3、切面类要使用注解@Components和@Aspect
4、定义方法,方法上使用注解表明这个方法的执行时机,有如下注解:
5、切面方法的注解中,要编写AspectJ表达式,告诉该方法要监听哪些方法;
6、如果要拿到目标方法的方法名称和参数,就可以在方法形参中直接写上:JoinPoint joinPoint
5、方法签名编写 AspectJ 切入点表达式
–execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法
在 AspectJ 中, 切入点表达式可以通过操作符 &&,||, ! 结合起来:
6、后置通知
在刚才的LoggingAspectJAop加上后置通知 @After:
/**
* 后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止
* @param joinPoint
*/
@After("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"执行完了");
}
7、返回通知
无论连接点是正常返回还是抛出异常, 后置通知都会执行。 如果只想在连接点返回的时候(方法正常结束)记录日志, 应使用返回通知代替后置通知。返回通知可以访问方法的返回值。
返回通知中, 只要将 returning 属性添加到 @AfterReturning 注解中,就可以访问连接点的返回值,该属性的值即为用来传入返回值的参数名称;必须在通知方法的签名中添加一个同名参数,在运行时,Spring AOP 会通过这个参数传递返回值;原始的切点表达式需要出现在 pointcut 属性中
在刚才的LoggingAspectJAop加上返回通知 @AfterReturning:
@AfterReturning(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))",returning = "result")
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
8、异常通知
只在连接点抛出异常时才执行异常通知;
将throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常。Throwable 是所有错误和异常类的超类, 所以在异常通知方法可以捕获到任何错误和异常。
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行。
/**
* 异常通知
*/
@AfterThrowing(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))",throwing = "exception")
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出现了异常,异常信息是:"+exception.getMessage());
}
9、环绕通知
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点;
对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint ,它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点;
在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法, 如果忘记这样做就会导致通知被执行了,但目标方法没有被执行
注意:环绕通知的方法 需要返回目标方法执行之后的结果,即调用 joinPoint.proceed(); 的返回值,否则会出现空指针异常
/**
* 环绕通知需要携带 ProceedingJoinPoint 类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值, 返回值即为目标方法的返回值
*/
@Around(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定义返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法执行之前,参数是:"+args);
result = pjd.proceed();
//后置通知
System.out.println(methodName+"方法执行完成,结果是:"+result);
} catch (Throwable throwable) {
//异常通知
System.out.println(methodName+"方法执行出现了异常,异常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"执行没有发生异常,一切正常返回,返回结果是:"+result);
return result;
}
10、切面的优先级
11、重(chong)用切入点定义
1、在编写AspectJ 切面时, 可以直接在通知注解中书写切入点表达式,但同一个切点表达式可能会在多个通知中重复出现;
2、在AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法。切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的;
3、切入点方法的访问控制符同时也控制着这个切入点的可见性,如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中,在这种情况下,它们必须被声明为 public;在引入这个切入点时,必须将类名也包括在内,如果类没有与这个切面放在同一个包中,还必须包含包名;
4、其他通知可以通过方法名称引入该切入点。
切面的完整代码:
package com.spring.aspectjAop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Order(0)//可以使用 @Order 注解指定切面的优先级, 值越小优先级越高
@Component//首先切面也是spring的一个Bean
@Aspect//表明这是一个AspectJ切面
public class LoggingAspectJAop {
/**
* 定义一个方法, 用于声明切入点表达式. 一般地, 该方法中再不需要添入其他的代码.
* 使用 @Pointcut 来声明切入点表达式.
* 后面的其他通知直接使用方法名来引用当前的切入点表达式.
*/
@Pointcut("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void JoinPointCut(){}
/**
* 标识这个方法是个前置通知, 切点表达式表示执行任意类的任意方法. 第
* 一个 * 代表匹配任意修饰符及任意返回值, 第二个 * 代表任意类的对象,
* 第三个 * 代表任意方法, 参数列表中的 .. 匹配任意数量的参数
*/
@Before("JoinPointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("执行的方法是:"+methodName+",参数是:"+args);
}
/**
* 后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止
* @param joinPoint
*/
@After("JoinPointCut()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"执行完了");
}
@AfterReturning(pointcut = "JoinPointCut()",returning = "result")
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
/**
* 异常通知
*/
@AfterThrowing(pointcut = "JoinPointCut()",throwing = "exception")
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出现了异常,异常信息是:"+exception.getMessage());
}
/**
* 环绕通知需要携带 ProceedingJoinPoint 类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值, 返回值即为目标方法的返回值
*/
@Around(value = "JoinPointCut()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定义返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法执行之前,参数是:"+args);
result = pjd.proceed();
//后置通知
System.out.println(methodName+"方法执行完成,结果是:"+result);
} catch (Throwable throwable) {
//异常通知
System.out.println(methodName+"方法执行出现了异常,异常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"执行没有发生异常,一切正常返回,返回结果是:"+result);
return result;
}
}
12、引入通知
引入通知是一种特殊的通知类型. 它通过为接口提供实现类, 允许对象动态地实现接口, 就像对象已经在运行时扩展了实现类一样。
引入通知示例代码:
3.3.2 基于xml配置文件使用AspectJ实现AOP
1、业务接口和业务实现类
package com.spring.aspectAop.xml;
public interface MathCalculator {
public int add(int a, int b);
public int sub(int a, int b);
}
package com.spring.aspectAop.xml;
import org.springframework.stereotype.Component;
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2、切面类
package com.spring.aspectAop.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
public class LoggingAspectJAop {
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("执行的方法是:"+methodName+",参数是:"+args);
}
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"执行完了");
}
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出现了异常,异常信息是:"+exception.getMessage());
}
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定义返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法执行之前,参数是:"+args);
result = pjd.proceed();
//后置通知
System.out.println(methodName+"方法执行完成,结果是:"+result);
} catch (Throwable throwable) {
//异常通知
System.out.println(methodName+"方法执行出现了异常,异常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"执行没有发生异常,一切正常返回,返回结果是:"+result);
return result;
}
}
3、配置文件
<?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-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置业务类Bean-->
<bean id="mathCalculator" class="com.spring.aspectAop.xml.MathCalculatorImpl"/>
<!--配置切面的 bean-->
<bean id="loggingAspectJAop" class="com.spring.aspectAop.xml.LoggingAspectJAop"/>
<!--配置AOP-->
<aop:config>
<!--配置切点表达式-->
<aop:pointcut id="joinPointCut" expression="execution(* com.spring.aspectAop.xml.*.*(..))"/>
<!--配置切面及通知-->
<aop:aspect ref="loggingAspectJAop" order="0">
<!--前置通知-->
<aop:before method="beforeMethod" pointcut-ref="joinPointCut"/>
<!--后置通知-->
<aop:after method="afterMethod" pointcut-ref="joinPointCut"/>
<!--异常通知-->
<aop:after-throwing method="afterThrowableMethod" pointcut-ref="joinPointCut" throwing="exception"/>
<!--返回通知-->
<aop:after-returning method="afterRunningMethod" pointcut-ref="joinPointCut" returning="result"/>
<!-- 环绕通知
<aop:around method="aroundMethod" pointcut-ref="joinPointCut"/>
-->
</aop:aspect>
</aop:config>
</beans>
4、测试
package com.spring.aspectAop.xml;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext-xml.xml");
MathCalculator mathCalculator = (MathCalculator) applicationContext.getBean("mathCalculator");
int add = mathCalculator.add(9, 3);
System.out.println("加法结果:"+add);
int sub = mathCalculator.sub(9, 3);
System.out.println("减法结果:"+sub);
}
}