前言
一直感觉网上很多教程和书本都把AOP写得比较复杂。因为这个概念涉及代理模式,通常会把静态代理和动态代理模式说一遍,然后AOP的概念讲一堆,但具体怎么使用却没能完整展示。因此自己决定写一篇文章简单介绍一下AOP的使用,略过一些概念性问题。
关于代理模式
讲AOP还是不能完全不提代理模式。静态代理和动态代理读者可以自行网上查找,不懂不太影响AOP的使用。AOP很重要一个作用是可以无入侵式地在执行一个函数前后增加处理逻辑。
举个例子,如果我们希望某个模块的函数被调用的时候都写日志,例如这样:
public class ClassA {
public void funA() {
log.error("进入方法funA");
System.out.println("执行方法A");
}
public void funB() {
log.error("进入方法funB");
System.out.println("执行方法B");
}
public void funC() {
log.error("进入方法funC");
System.out.println("执行方法C");
}
}
在一个大型工程中,这样的写法通常存在几个问题:第一,如果要在进入这些函数时再增加一些共同的函数处理,需要在给每个函数再加一个方法调用,不但繁琐而且很容易遗漏;第二,每次增加、删除记录日志这类调用,都需要修改业务代码,但这些调用和业务处理没什么关系,对代码入侵太强;第三,如果每次进入函数的前后都要进行数种处理,那代码就会显得非常臃肿,可读性极差。其他各类问题还有不少,就不叙述。总之最好有一种方式能解决这些问题。因此就有了代理模式。Spring的AOP也是解决这样的问题。
Spring AOP的三种方式
首先我们定义一个类
public class LoginController {
public void login(String userName) {
System.out.println("用户 " + userName + " 登入");
}
public void logout(String userName) {
System.out.println("用户 " + userName + " 登出");
}
}
假设用户登入时,调用login函数,用户登出时调用logout函数。我们在这个例子中使用AOP的目的,就是在调用login和logout函数的前后,增加一些处理逻辑。
Spring AOP的API方式
这种方式比较少用,就当作了解。我们实现MethodBeforeAdvice的接口,这个表示当调用某个指定函数时,调用函数前执行这个before方法:
public class BeforeLog implements MethodBeforeAdvice{
public void before(Method m, Object[] arg, Object target) throws Throwable {
System.out.println("进入方法前");
}
}
另外再实现一个AfterReturningAdvice的接口,这个表示当调用某个指定函数时,调用函数返回后执行这个afterReturning方法:
public class AfterLog implements AfterReturningAdvice{
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("方法返回后");
}
}
编写spring-aop1.xml,设置pointcut的表达式为LoginController的任何方法。设置pointcut关联beforeLog和afterLog通知。即执行LoginController的任何方法时,会触发beforeLog和afterLog通知
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="beforeLog" class="com.sadoshi.springtest.aop.BeforeLog" />
<bean id="afterLog" class="com.sadoshi.springtest.aop.AfterLog" />
<bean id="loginController" class="com.sadoshi.springtest.aop.LoginController" />
<!-- 方法一 Spring的API接口 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut id="pointcut" expression="execution(* com.sadoshi.springtest.aop.LoginController.*(..))"/>
<!-- 执行环绕 -->
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
编写一个简单的主函数调用login方法
public class AopApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop1.xml");
LoginController contoller = context.getBean("loginController", LoginController.class);
contoller.login("James");
}
}
执行后结果:
输出显然已经达到了我们的目的。在login前先调用了BeforeLog的before方法,在调用login后执行AfterLog的afterReturning。
自定义切面方式
第一种方式有一个问题,就是每一个advise处理,都要定义一个类。如果有多个处理,那就要定义很多类,大型项目中显得非常臃肿。自定义切面的方式,可以解决这个问题。
首先这种方法和下面一种方法都需要在maven中添加依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
<scope>runtime</scope>
</dependency>
创建一个自定义切面类:
public class CustomAspect {
public void before(JoinPoint jp) {
System.out.println("进入方法" + jp.getSignature().getName() + "前");
}
public void after() {
System.out.println("方法执行后");
}
public void afterReturning() {
System.out.println("方法返回后");
}
}
JointPoint这个形参可有可无,带上的话,可以从中取得一些信息,例如当前拦截的函数名、对象信息等等
然后编写spring-aop2.xml,指定aspect为customAspect实例。 在aspect内部定义pointcut,并且指定切面的before、after和after-returning对应的方法与pointcut关联。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="customAspect" class="com.sadoshi.springtest.aop.CustomAspect" />
<bean id="loginController" class="com.sadoshi.springtest.aop.LoginController" />
<!-- 方法二 自定义切面 -->
<aop:config>
<aop:aspect ref="customAspect">
<aop:pointcut id="pointcut" expression="execution(* com.sadoshi.springtest.aop.LoginController.*(..))"/>
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
同样简单调用login方法
public class AopApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop2.xml");
LoginController contoller = context.getBean("loginController", LoginController.class);
contoller.login("James");
}
}
执行后的输出结果符合预期。这种方式的好处是可以把处理函数都集中到一个类中,简洁很多,工程中常用。
注解方式
注解方式也是工程常用,定义一个切面类:
@Aspect
public class AnnoLog {
@Before("execution(* com.sadoshi.springtest.aop.LoginController.*(..))")
public void before() {
System.out.println("调用方法前");
}
@After("execution(* com.sadoshi.springtest.aop.LoginController.*(..))")
public void after() {
System.out.println("调用方法后");
}
}
注解@Aspect标注这是一个切面,@Before代表是切点前执行的方法,括号内是切点。
编写spring-aop3.xml。使用AOP注解,必须添加<aop:aspectj-autoproxy/>这句。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="loginController" class="com.sadoshi.springtest.aop.LoginController" />
<bean id="annoLog" class="com.sadoshi.springtest.aop.AnnoLog" />
<aop:aspectj-autoproxy/>
</beans>
之后像之前那样调用LoginController的login方法即可,这里就省略了。
小结
AOP的概念比较复杂,因此我的文章就不详细说了,但是使用并不是很难理解,上手十分轻松。AOP相关的使用在JAVA非常普遍,很多框架、组件都使用了AOP,因此熟练AOP的使用有助于加深各种组件、框架的理解。