Spring-(八)AOP的实现

本文最后更新于:June 3, 2022 pm

Spring 是目前主流的 Java Web 开发框架,是 Java 世界最为成功的框架。Spring框架是由于软件开发的复杂性而创建的。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅仅限于服务器端的开发。从简单性、可测试性和松耦合性角度而言,绝大部分Java应用都可以从Spring中受益。该框架是一个轻量级的开源框架,具有很高的凝聚力和吸引力。Spring 框架不局限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。

目录

环境准备

使用AOP,需要添加一个依赖:

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8.RC3</version>
</dependency>

UserService.java

1
2
3
4
5
6
7
8
9
package com.tothefor.aopTest;

public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tothefor.aopTest;

public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加-了一个用户");
}

@Override
public void delete() {
System.out.println("删除-了一个用户");
}

@Override
public void update() {
System.out.println("修改-了一个用户");
}

@Override
public void query() {
System.out.println("查询-了一个用户");
}
}

现在有一个需求:添加打印日志业务,但不能修改原有业务逻辑。

实现方式一

使用Spring的原生API接口。Spring中支持5种类型的Advice:

在方法前输出日志,LogBefore.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tothefor.aopTest;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class LogBefore implements MethodBeforeAdvice {

//method:要执行的目标对象的方法
//args:参数
//target:目标对象
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("LogBefore: "+target.getClass().getName()+"的"+method.getName()+"方法,执行了");
}
}

在方法后输出日志,LogAfter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.tothefor.aopTest;

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

public class LogAfter implements AfterReturningAdvice {

//returnValue:参数方法的返回值
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("LogAfter: "+"执行了"+method.getName()+"方法,返回结果为"+returnValue);
}
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置bean-->
<bean id="userService" class="com.tothefor.aopTest.UserServiceImpl"/>
<bean id="logBefore" class="com.tothefor.aopTest.LogBefore"/>
<bean id="logAfter" class="com.tothefor.aopTest.LogAfter"/>

<!-- 配置aop,需要导入aop的约束-->
<aop:config>
<!-- 切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.tothefor.aopTest.UserServiceImpl.*(..))"/>

<!-- 执行环绕增加-->
<aop:advisor advice-ref="logBefore" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="pointcut"/>

</aop:config>
</beans>

解释

首先,需要配置bean,包括在哪一个类(方法)切入,切入哪一个类(方法)。

1
2
3
4
<!--    配置bean-->
<bean id="userService" class="com.tothefor.aopTest.UserServiceImpl"/>
<bean id="logBefore" class="com.tothefor.aopTest.LogBefore"/>
<bean id="logAfter" class="com.tothefor.aopTest.LogAfter"/>

然后配置aop,但需要先导入aop的约束:(头部加入)

1
2
3
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd

然后再配置切入点

1
2
3
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.tothefor.aopTest.UserServiceImpl.*(..))"/>
</aop:config>

execution(访问权限 方法返回值 方法声明(参数) 异常类型) 可以简化为:execution(方法返回值 方法声明(参数))

上面这表示切入点是:com.tothefor.aopTest.UserServiceImpl.*(..)。UserServiceImpl类中的所有方法。后面的(..)表示方法的参数,这里写的是两个点(..),表示任意参数。前面的星号(*)表示所有的方法,这里的星号也可以改成固定的方法名。最前面的一个星号表示修饰符任意。

可以配置多个切入点。

既然在哪儿个位置切入(插入)确定,现在就应该配置(确定)应该切入(插入)什么了,

1
2
3
4
5
6
7
<aop:config>
<!-- 切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.tothefor.aopTest.UserServiceImpl.*(..))"/>
<!-- 执行环绕增加-->
<aop:advisor advice-ref="logBefore" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="pointcut"/>
</aop:config>

advice-ref表示应该切入(插入)什么,而pointcut-ref表示在哪一个切入点切入。

切入点表达式别名

2022年6月3日

当有多个切入点相同时,可以使用别名。例如:

1
2
3
4
5
6
7
8
9
10
11
@Aspect
public class aspectJAOP {

@Before(value = "cutM()") //使用别名
public void log(){
System.out.println("这里是附加信息");
}

@Pointcut(value = "execution(* *(..))") //定义别名
public void cutM(){}
}

写一个空方法,使用@Pointcut来设置切入点表达式,方法的名称即为别名。见上面代码示例。

测试

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService"); //用接口接收,不能用其实现类
userService.add();
}

注意:动态代理的是接口!不能写类!

输出:

1
2
3
LogBefore: com.tothefor.aopTest.UserServiceImpl的add方法,执行了
增加-了一个用户
LogAfter: 执行了add方法,返回结果为null

实现方式二(自定义类)

通过自定义类实现AOP。这种方式就是将所有的要插入的东西都写在一个类中,然后这个类就形成一个切入面。

Log.java

1
2
3
4
5
6
7
8
9
10
11
package com.tothefor.aopTest02;

public class Log {
public void before(){
System.out.println("插在了前面");
}
public void after(){
System.out.println("插在了后面");
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置bean-->
<bean id="userService" class="com.tothefor.aopTest.UserServiceImpl"/>
<bean id="log" class="com.tothefor.aopTest02.Log"/>

<!-- 配置aop-->
<aop:config>
<!-- 自定义切面,ref 是要引用的类-->
<aop:aspect ref="log">
<!-- 切入点设置-->
<aop:pointcut id="point" expression="execution(* com.tothefor.aopTest.UserServiceImpl.*(..))"/>
<!-- 通知-->
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
</aop:aspect>
</aop:config>
</beans>

其中,method需要写自定义类中的方法名。

测试

1
2
3
4
5
6
7
8
9
10
11
12
import com.tothefor.aopTest.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

输出:

1
2
3
插在了前面
增加-了一个用户
插在了后面

实现方式三(注解)

也是用一个自定义类实现,只不过是使用注解的方式。

autoTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tothefor;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect //表示这类是一个切面
public class autoTest {
@Before("execution(* com.tothefor.aopTest.UserServiceImpl.*(..))")
public void before(){
System.out.println("在方法之前执行");
}
}

@After使用同理。

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置bean-->
<bean id="userService" class="com.tothefor.aopTest.UserServiceImpl"/>
<bean id="autot" class="com.tothefor.autoTest"/>
<!-- 开启注解支持-->
<aop:aspectj-autoproxy/>

</beans>

测试

同上测试代码一样。

总结

切入分三步:

    1. 确定具体在哪个位置插入(切入)。
    1. 要插入(切入)什么。
    1. 指定关系。(谁插在谁前面或后面?)

两种方式的区别:

  • 第一种方式中可以拿到相关方法的名称和参数等,至少和相关方法有关系。
  • 第二种方式中就只是单纯的切入方法,和相关方法没有一点关系。