为什么使用AOP?原始做法存在以下问题
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同事还必须兼顾其他多个关注点。
代码分散:以日志需求为例,只是为了满足这个单一的需求,就不得不在多个模块(方法)里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有模块。
使用动态代理可以实现上述需求,在代理类执行方法前后添加日志操作。
AOP(面向切面编程):是一种方法论,对传统的OOP(面向对象编程)的补充,AOP的主要编程对象是切面(aspect)
切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象(类比上图把验证、日志单独抽取出来成为一个类)
通知(Advice):切面必须要完成的工作(上图验证参数,前置日志,后置日志,是需要做的工作)
目标(Target):被通知的对象(业务逻辑)
代理(Proxy):向目标对象应用通知之后创建的对象(将验证参数,前置日志,后置日志和业务逻辑相结合之后创建的对象,和动态代理类似)
连接点(Joinpoint):程序执行的某个特定位置,如类某个方法调用前、调用后、方法抛出异常后等。
连接点由两个信息确定:方法表示的程序执行点,相对点表示的方位,例如ArithAop.add()方法执行前的连接点,执行点为ArithAop.add(),方位为该方法执行前的位置。
切点(Pointcut):每个类都有多个连接点,例如ArithAop中所有的方法实际上都是连接点,即连接点是程序类中客观存在的。AOP通过切点定位到特定的连接点。就好比连接点是是数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
在Spring中启用AspectJ注解支持
要在Spring应用中使用AspectJ注解,必须在classpath添加AspectJ 类库,主要依赖于aopalliance.jar,aspectj.weaver.jar和spring-aspects.jar
将aop 命名空间添加到XML文件中,要在SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML元素aop:aspectj-autoproxy
将SpringIOC容器侦测到Bean配置文件中的aop:aspectj-autoproxy元素时,会自动为与AspectJ切面匹配的Bean创建代理。
将横切关注点的代码抽象AOP切面类中,定义切面,同时也是SpringIOC容器的Bean
@Component
@Aspect
public class LogAspect {
@Before("execution(public int com.cmzy.spring.aop.ArithAopImpl.add(int,int))")
public void beforeMethod() {
System.out.println("在方法执行之前执行。。。。。");
}
}
在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,通知是标注有某种注解的简单的Java方法。
AspectJ支持5种类型的通知注解:
@Before:前置通知,在方法执行之前执行
@After:后置通知,在方法执行之后执行(后置通知是在连接完成之后执行的,即连接点返回结果或者抛出异常的时候,后置通知都会执行,相当于在finally中执行,并且在后置通知中不能访问目标方法的执行结果)
@AfterRunning:返回通知,在方法返回结果之后执行(在方法正常结束后执行的,返回通知是可以访问到方法的返回值)
@AfterThrowing:异常通知,在方法抛出异常之后
@Around:环绕通知,围绕着方法执行
通知注解中的表示语法(execution)
execution(* com.cmzy.spring.aop.ArithAopImpl.(…)):表示匹配ArithAopImpl中声明的所有方法,第一个参数代表任意修饰符及任意返回值。第二个代表任意方法,…表示匹配任意数量的参数。若目标类和接口与该切面在同一个包中,则可以省略包名。
execution(public * com.cmzy.spring.aop.ArithAopImpl.(…)):表示匹配ArithAopImpl的所有public 方法。
execution(public double com.cmzy.spring.aop.ArithAopImpl.(…)):表示匹配ArithAopImpl中所有public的,返回double的方法
execution(public double com.cmzy.spring.aop.ArithAopImpl.*(double,…)):表示匹配ArithAopImpl中所有public的,返回double的,并且第一个参数为double类型的,后面匹配任意类型的参数的方法。
如果需要在通知方法中访问连接点的细节,如访问方法的签名,方法的参数等,可以在通知方法中声明一个类型为JoinPoint的参数。
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(public int com.cmzy.spring.aop.ArithAop.add(int,int))")
public void pointCutTest() {
}
@Before("pointCutTest()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("在方法执行之前执行。。。。。");
}
@After("execution(* ArithAopImpl.*(..))")
public void after() {
System.out.println("在方法执行后执行。。。。。");
}
}
返回通知的实现,可以获取返回值
@AfterReturning(value = "execution(* ArithAopImpl.*(..))",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("返回结果是"+ result);
}
对比动态代理,可以看到相应的通知处在什么位置,其作用等
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// TODO Auto-generated method stub
Object obj = null;
try {
//前置通知
obj = method.invoke(target, args);
//返回通知,可以访问到方法的返回值
} catch (Exception e) {
//异常通知
}
//后置通知
return obj;
}
异常通知:在目标方法出现异常时会执行的代码,可以访问到异常对象,且可以指定在出现特定异常时执行相应代码,比如下面异常通知定义时是使用的Exception拦截所有异常,如果在此处定义NullPointException,而实际抛出的是其他的异常,那么该通知就不会执行。
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* ArithAopImpl.*(..))")
public void pointCutTest() {
}
@Before("pointCutTest()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("在方法执行之前执行。。。。。");
}
@After("pointCutTest()")
public void after() {
System.out.println("在方法执行后执行。。。。。");
}
@AfterReturning(value = "pointCutTest()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("返回结果是"+ result);
}
@AfterThrowing(pointcut = "pointCutTest()",throwing = "ex")
public void afterThrow(Exception ex) {
ex.printStackTrace();
}
}
环绕通知:环绕通知需要携带ProceedingJoinPoint类型的参数,环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法,且环绕通知必须有返回值,返回值即为目标方法的返回值。
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* ArithAopImpl.*(..))")
public void pointCutTest() {
}
@Around(value = "pointCutTest()")
public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
System.out.println("类似前置通知。。。。。");
result = pjp.proceed();
System.out.println("类似返回通知。。。。。");
} catch (Throwable e) {
// TODO Auto-generated catch block
System.out.println("类似异常通知。。。。。");
e.printStackTrace();
}
System.out.println("类似后置通知。。。。");
return result;
}
}
切面优先级:当多个切面都用于同一个方法,那么可以设置切面的执行的先后顺序,在切面定义的时候使用@Order(number)注解,number值越小,优先级越高。
切面里面定义@PointCut注解可以用于被引用,如果不是在同一个切面中,其他切面需要引用,可以使用:切面的全类名.切点方法(),这种形式引用
常见的切面表达式
1 所有公有方法的执行
execution(public * *(..))
2 所有以set开头的公有方法的执行
execution(* set*(..))
3 AccountService接口下的所有方法的执行
execution(* com.xyz.service.AccountService.*(..))
4 com.xyz.service包下的所有方法的执行
execution(* com.xyz.service.*.*(..))
5 com.xyz.service包及其子包下的所有方法的执行
execution(* com.xyz.service..*.*(..))
6 匹配com.xyz.service包下的所有类的所有方法(不含子包)
within(com.xyz.service.*)
7 com.xyz.service包和子包的所有方法
within(com.xyz.service..*)
8 匹配AccountService的代理类(不支持通配符)
this(com.xyz.service.AccountService)
基于XML配置形式:
<!-- 目标对象 -->
<bean id="arithXMLAop" class="com.cmzy.spring.aop.ArithXMLAopImpl"></bean>
<!-- 切面 -->
<bean id="logXmlAspect" class="com.cmzy.spring.aop.LogXmlAspect"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切点 -->
<aop:pointcut expression="execution(public * *(..))" id="xmlAspect"/>
<!-- 配置增强,即切点和 通知的组成-->
<aop:aspect ref="logXmlAspect">
<aop:before method="beforeMethod" pointcut-ref="xmlAspect"/>
<aop:after method="afterMethod" pointcut-ref="xmlAspect"/>
<aop:after-returning method="" pointcut-ref="" returning=""/>
<aop:after-throwing method="" pointcut-ref="" throwing=""/>
<aop:around method=" " pointcut-ref=""/>
</aop:aspect>
</aop:config>