代码混乱:越来越多的非业务需求(日志、验证)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时,还要必须兼顾其他多个关注点。
代码分散:以打印日志为例,为了满足打印日志需求,需要在多个模块里多次编写重复相同的日志代码,如果日志需求发生变化,就必须修改所有模块。
这里为了解决此类问题,其解决方案有点像代理设计模式:使用一个代理,将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。
基于这种设计思想,Spring发明了AOP。
AOP(Aspect-Oriented Programming,面向切面编程),是一种新的方法论,是对传统OOP即面向对象编程的补充。
AOP主要编程对象是切面aspect,而切面模块化横切关注点。
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里、以什么方式应用,并且不必修改受影响的现有类,这样一来横切关注点就被模块化到特殊的切面对象里。
这样一来AOP主要好处有2个:
①每个事物逻辑位于一个位置,代码不分散,便于维护和升级;
②业务模块更简洁,只包含核心业务代码;
AOP常用术语如下:
切面Aspect:横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象;
通知Advice:切面必须要完成的工作;
目标Target:被通知的对象;
代理Proxy:向目标对象应用通知之后创建的对象;
连接点joinpoint:程序执行的某个特定位置,如果某个方法调用前、调用后、方法抛出异常后等。连接点由2个信息来确定:方法表示的程序执行点、相对点表示的方位;
切点pointcut:每个类都拥有多个连接点,即连接点是程序类中客观存在的事务。AOP通过切点来定位到特定的连接点,举个例子:连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一关系,一个切点可以匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法来作为连接点的查询条件;
AspectJ是JAVA社区中最完整也是最流行的AOP框架,在Spring2.0以上版本中,可以使用基于AspectJ注解或者基于XML配置的AOP。
要在Spring应用中使用AspectJ注解,需要经过如下步骤:
①在classpath下包含AspectJ类库:aopalliance.jar
/aspectj.weaver.jar/spring-aspects.jar,例如从AspectJ官网https://www.eclipse.org/aspectj/downloads.php下载对应的jar包,解压jar后在lib目录下找到上述的3个文件, 然后在idea中配置:File-Project Structure-Modules-dependencies-jars or directories
②将aopSchema添加到<beans/>根元素中;
③在<beans/>配置文件中定义一个空的XML元素<aop:aspectj-autoproxy/>标签,这样当Spring IOC容器检测到配置文件中的该aspectj标签时,会自动为那些与AspectJ切面匹配的Bean类来创建代理;
④把某个类声明为一个切面:首先把这个类放到IOC容器中,即使用@Component注解来标注,然后再声明为一个切面,即使用@Aspect注解;
⑤根据业务需要,选择下面某种通知:
@Before(前置通知,在方法执行之前执行)
@After(后置通知,方法执行之后执行,无论方法是否发生异常)
@AfterRunning(返回通知,在方法正常执行结束、返回结果之后执行,该通知可以访问方法所返回的结果)
@AfterThrowing(异常通知,在方法抛出异常之后执行,该通知可以访问方法发生的异常,且可以指定在出现特定异常时再去执行通知)
@Around(环绕通知,围绕着方法执行);
@Component @Aspect public class LoggingAspect { /** * 前置通知,在方法执行之前执行 * */ @Before("execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))") public void beforeMethod( JoinPoint joinpoint){ System.out.println("beforeMethod " + "method name = " + joinpoint.getSignature().getName() + ", method args = " + Arrays.asList(joinpoint.getArgs()).toString()); } /** * 后置通知,方法执行之后执行,无论方法是否发生异常 * */ @After("execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))") public void afterMethod(JoinPoint joinpoint){ System.out.println("afterMethod method name = " + joinpoint.getSignature().getName() + " end"); } /** * 返回通知,在方法正常执行结束、返回结果之后执行,该通知可以访问方法所返回的结果 * */ @AfterReturning( value="execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))", returning = "result") public void afterReturningMethod(JoinPoint joinpoint,Object result){ System.out.println("afterReturningMethod method name = " + joinpoint.getSignature().getName() + " end with " + result); } /** * 异常通知,在方法抛出异常之后执行,该通知可以访问方法发生的异常,且可以指定在出现特定异常时再去执行通知 * */ @AfterThrowing( value="execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinpoint, NullPointerException ex){ System.out.println("afterThrowingMethod method name = " + joinpoint.getSignature().getName() + " exception with" + ex); }
/** * 环绕通知,围绕着方法执行 * 该通知必须使用ProceedingJoinPoint类型的参数,环绕通知可以看做是:前置 + 后置 + 返回 + 异常 * ProceedingJoinPoint可以决定是否执行目标方法,且环绕通知必须有返回值,返回值为目标方法的返回值 * */ @Around("execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))") public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint ){ Object result = null; try { // 执行前置通知 System.out.println("aroundMethod beforeMethod " + "method name = " + proceedingJoinPoint.getSignature().getName() + ", method args = " + Arrays.asList(proceedingJoinPoint.getArgs()).toString()); // 执行目标方法 result = proceedingJoinPoint.proceed(); // 返回通知 System.out.println("aroundMethod afterReturningMethod method name = " + proceedingJoinPoint.getSignature().getName() + " end with " + result); }catch (Throwable throwable) { // throwable.printStackTrace(); // 异常通知 System.out.println("aroundMethod afterThrowingMethod method name = " + proceedingJoinPoint.getSignature().getName() + " exception with" + throwable); }finally { // 后置通知 System.out.println("aroundMethod afterMethod method name = " + proceedingJoinPoint.getSignature().getName() + " end"); } return result; } } |
aroundMethod beforeMethod method name = getOrgByName, method args = [102100088885] beforeMethod method name = getOrgByName, method args = [102100088885] ............OrgService.getOrgByName start ............OrgService.getOrgByName end aroundMethod afterReturningMethod method name = getOrgByName end with Org{orgCode='null', orgName='102100088885', areaCode='null', telphone='null'} aroundMethod afterMethod method name = getOrgByName end afterMethod method name = getOrgByName end afterReturningMethod method name = getOrgByName end with Org{orgCode='null', orgName='102100088885', areaCode='null', telphone='null'} ............OrgService.getOrgByName return |
当多个切面,其内部AspectJ表达式关注的切点是同一个时,可以通过@Order(数字)注解来表示优先级,其中数字值越小,其切面优先级越高,越优先执行。
如果想复用现有的AspectJ表达式,需要如下步骤:
①定义一个空方法,并且使用@Pointcut注解来定义表达式;
②在本文件/其他包路径下直接使用方法名来引用当前的切入点表达式;
@Component @Aspect @Order(1) public class HeightAspect {
@Pointcut("execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgService.getOrgByName(java.lang.String))") public void AspectJPointCut(){}
/** * 前置通知,在方法执行之前执行 * */ @Before("com.suntj.spring.aop.aspect.HeightAspect.AspectJPointCut()") // 同一个文件下可以这么写 // @Before("AspectJPointCut()") public void beforeMethod( JoinPoint joinpoint){ System.out.println("HeightAspect beforeMethod " + "method name = " + joinpoint.getSignature().getName() + ", method args = " + Arrays.asList(joinpoint.getArgs()).toString()); } } |
<?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:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p" 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/util http://www.springframework.org/schema/util/spring-util-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<bean id="org" class="com.suntj.spring.aop.Org"></bean> <bean id="orgServiceByXML" class="com.suntj.spring.aop.impl.OrgServiceByXML" p:org-ref="org"></bean>
<!-- 配置切面bean --> <bean id="loggingAspectByXML" class="com.suntj.spring.aop.aspect.LoggingAspectByXML"></bean>
<!-- 配置AOP --> <aop:config> <!-- 配置切点表达式 --> <aop:pointcut id="getOrgByName" expression="execution(public com.suntj.spring.aop.Org com.suntj.spring.aop.impl.OrgServiceByXML.getOrgByName(java.lang.String))"/> <!-- 配置切面及通知 --> <aop:aspect ref="loggingAspectByXML" order="1"> <aop:before method="beforeMethod" pointcut-ref="getOrgByName"/> <aop:after method="afterMethod" pointcut-ref="getOrgByName"/> <aop:after-throwing method="afterThrowingMethod" pointcut-ref="getOrgByName" throwing="ex"/> <aop:after-returning method="afterReturningMethod" pointcut-ref="getOrgByName" returning="result"/> <aop:around method="aroundMethod" pointcut-ref="getOrgByName"/> </aop:aspect> </aop:config> </beans> |