AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处都基本相似,比如权限认证、日志、事务处理。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP中的一些名词如下。
@SpringBootApplication是spring boot最重要的一个注解,用于快捷配置启动类。相当于@Configuration+@EnableAutoConfiguration+@ComponentScan的组合。
@Configuration的注解类标识这个类可以使用Spring IOC容器作为Bean定义的来源。
@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册为在Spring应用程序上下问中的Bean。
@EnableAutoConfiguration:能够自动配置Spring的上下文,试图猜测和配置你想要的Bean类,通常会自动根据类路径和Bean定义自动配置。
这几个注解都是用于类的前面
@ComponentScan会自动扫描指定包下的全部标有@Component的类,并注册成Bean,当然包括@Component下的子注解@Service、@Repository和@Controller。
@SpringBootTest 用于标注一个Spring Boot的单元测试类,其中参数
在STS中新建一个Sping Boot项目,详细参考我之前的一篇博文:
微服务之Sprint Boot2—一切从简单开始
在pom.xml中增加spring-boot-starter-aop,如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
日志框架的性能比较:Log4j
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-log4j2artifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-yamlartifactId>
dependency>
在项目中新建3个包:
package com.wongoing.aop;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAopAdvice {
}
2、定义切入点,在AOP类的方法上加入@Pointcut,代码如下:
/**
* 功能说明:切入点定义
* 修改说明:
* @author zheng
* @date 2020-7-15 9:40:00
*/
@Pointcut("execution(* com.wongoing.service.*.*(..))")
public void executeService() {
}
@Pointcut:声明一个切入点,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。Spring AOP只支持Spring Bean的方法执行连接点。所以你可以把切入点看作Spring Bean上方法执行的匹配。
一个切入点声明有两个部分:
/**
* 功能说明:前置通知,通过方法签名的方式引用上面的切入点定义
* 修改说明:
* @author zheng
* @date 2020-7-15 9:40:21
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint) {
logger.info("第一个前置通知执行了...");
logger.info("-------------------------------");
}
@Before:前置通知——在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
后置返回通知一般是在业务处理完成,需要给用户返回统一的结果时触发,触发的方式和前置通知基本相同。代码如下:
/**
* 功能说明:后置通知
* 修改说明:
* @author zheng
* @date 2020-7-15 9:39:48
* @param joinPoint
* @param keys
*/
@AfterReturning(value = "execution(* com.wongoing.service.*.*(..))", returning = "keys")
public void doAfterReturningAdvice(JoinPoint joinPoint, Object keys) {
logger.info("--------------------------------");
logger.info("第一个后置返回通知的返回值:" + keys);
}
@AfterReturning:后置通知——在某连接点正常完成后执行的通知,通常在一个匹配的方法返回时执行。
后置异常通知一般在统一业务异常处理时使用,当业务收到某种异常时进行相应的处理。后置异常通知的配置方式如下:
/**
* 功能说明:后置异常通知
* 修改说明:
* @author zheng
* @date 2020-7-15 9:44:15
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "execution(* com.wongoing.service.*.doAfterThrowing*(..))", throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
//输出目标方法名
logger.info(joinPoint.getSignature().getName());
if (exception instanceof NullPointerException) {
logger.info("发生了空指针异常!!!");
}
}
@AfterThrowing:异常通知——在方法抛出异常退出时执行的通知。
后置最终通知的配置方式如下:
/**
* 功能说明:后置最终通知
* 修改说明:
* @author zheng
* @date 2020-7-15 15:00:58
* @param joinPoint
*/
@After(value = "execution(* com.wongoing.service.*.*(..))")
public void doAfterAdvice(JoinPoint joinPoint) {
logger.info("后置最终通知执行了!!!");
}
@After:最终通知——当某连接点退出时执行的通知(不论是正常返回还是异常退出)。
环绕通知的配置方式如下:
/**
* 功能说明:环绕通知
* 修改说明:
* @author zheng
* @date 2020-7-15 15:01:12
* @param proceedingJoinPoint
* @return
*/
@Around(value = "execution(* com.wongoing.service.*.doAround*(..))")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
logger.info("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName());
try {
} catch(Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
@Around:环绕通知——包围一个连接点的通知,如方法调用,这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点,或直接返回它自己的返回值,或抛出异常来结束执行。
1、完整通知类的代码如下:
package com.wongoing.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAopAdvice {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 功能说明:切入点定义
* 修改说明:
* @author zheng
* @date 2020-7-15 9:40:00
*/
@Pointcut("execution(* com.wongoing.service.*.*(..))")
public void executeService() {
}
/**
* 功能说明:前置通知,通过方法签名的方式引用上面的切入点定义
* 修改说明:
* @author zheng
* @date 2020-7-15 9:40:21
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint) {
logger.info("第一个前置通知执行了...");
logger.info("-------------------------------");
}
/**
* 功能说明:后置通知
* 修改说明:
* @author zheng
* @date 2020-7-15 9:39:48
* @param joinPoint
* @param keys
*/
@AfterReturning(value = "execution(* com.wongoing.service.*.*(..))", returning = "keys")
public void doAfterReturningAdvice(JoinPoint joinPoint, Object keys) {
logger.info("--------------------------------");
logger.info("第一个后置返回通知的返回值:" + keys);
}
/**
* 功能说明:后置异常通知
* 修改说明:
* @author zheng
* @date 2020-7-15 9:44:15
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "execution(* com.wongoing.service.*.doAfterThrowing*(..))", throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
//输出目标方法名
logger.info(joinPoint.getSignature().getName());
if (exception instanceof NullPointerException) {
logger.info("发生了空指针异常!!!");
}
}
/**
* 功能说明:后置最终通知
* 修改说明:
* @author zheng
* @date 2020-7-15 15:00:58
* @param joinPoint
*/
@After(value = "execution(* com.wongoing.service.*.*(..))")
public void doAfterAdvice(JoinPoint joinPoint) {
logger.info("后置最终通知执行了!!!");
}
/**
* 功能说明:环绕通知
* 修改说明:
* @author zheng
* @date 2020-7-15 15:01:12
* @param proceedingJoinPoint
* @return
*/
@Around(value = "execution(* com.wongoing.service.*.doAround*(..))")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
logger.info("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName());
try {
} catch(Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
2、在com.wongoing.service包下创建一个业务类UserService.java,代码如下:
package com.wongoing.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* 功能说明:用户业务逻辑处理类
* 修改说明:
* @author zheng
* @date 2020-7-15 8:58:09
* @version 0.1
*/
@Service
public class UserService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 功能说明:用户登录
* 修改说明:
* @author zheng
* @date 2020-7-15 8:57:40
* @param userName 用户名
* @param userPass 用户密码
* @return 成功返回true,失败返回false
*/
public boolean login(String userName, String userPass) {
this.logger.info("正在执行用户登录方法...");
if (userName.equals("admin") && userPass.equals("123456")) {
return true;
} else {
return false;
}
}
/**
* 功能说明:测试后置异常通知的业务方法
* 修改说明:
* @author zheng
* @date 2020-7-15 9:51:31
*/
public void doAfterThrowingMethod() {
this.logger.info("正在执行一个业务方法....1");
throw new NullPointerException();
}
/**
* 功能说明:测试环绕通知的方法
* 修改说明:
* @author zheng
* @date 2020-7-15 9:53:32
*/
public void doAroundMethod() {
this.logger.info("正在执行业务方法...2");
}
}
3、在com.wongoing.controller包下创建一个UserController.java控制器类,代码如下:
package com.wongoing.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wongoing.service.UserService;
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/")
public String home() {
if (this.userService.login("admin", "123456")) {
return "欢迎使用Spring Boot!";
} else {
return "登录失败,请重试!";
}
}
}
4、修改主类,增加@ComponentScan,代码如下:
package com.wongoing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = "com.wongoing.aop,com.wongoing.service,com.wongoing.controller")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
5、在项目的src\test\java目录下新建包
package com.wongoing.service.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.wongoing.DemoApplication;
import com.wongoing.service.UserService;
@SpringBootTest(classes = DemoApplication.class)
public class UserServiceTests {
@Autowired
private UserService service;
@Test
public void testLogin() {
boolean result = this.service.login("admin", "123456");
Assertions.assertEquals(result, true);
}
@Test
public void testDoAfterThrowingMethod() {
this.service.doAfterThrowingMethod();
}
@Test
public void testDoAroundMethod() {
this.service.doAroundMethod();
}
}
7、在com.wongoing.controller.test包下创建单元测试类UserControllerTests.java,代码如下:
package com.wongoing.controller.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import com.wongoing.DemoApplication;
@SpringBootTest(classes = DemoApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testHome() {
String testUrl = "/";
ResponseEntity<String> entity = this.restTemplate.getForEntity(testUrl, String.class);
Assertions.assertEquals(entity.getStatusCode(), HttpStatus.OK);
Assertions.assertEquals(entity.getBody(), "欢迎使用Spring Boot!");
}
}
8、以JUnit方式运行UserServiceTests.testLogin()方法,输出日志如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
2020-07-15 15:44:27.726 INFO 20672 --- [ main] c.w.s.t.UserServiceTests : Starting UserServiceTests on zhenglibing-pc with PID 20672 (started by zheng in E:\sts-workspace\demo)
2020-07-15 15:44:27.739 INFO 20672 --- [ main] c.w.s.t.UserServiceTests : No active profile set, falling back to default profiles: default
2020-07-15 15:44:29.800 INFO 20672 --- [ main] o.s.s.c.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-15 15:44:30.310 INFO 20672 --- [ main] c.w.s.t.UserServiceTests : Started UserServiceTests in 2.92 seconds (JVM running for 4.094)
2020-07-15 15:44:30.613 INFO 20672 --- [ main] c.w.a.MyAopAdvice : 第一个前置通知执行了...
2020-07-15 15:44:30.613 INFO 20672 --- [ main] c.w.a.MyAopAdvice : -------------------------------
2020-07-15 15:44:30.674 INFO 20672 --- [ main] c.w.s.UserService : 正在执行用户登录方法...
2020-07-15 15:44:30.674 INFO 20672 --- [ main] c.w.a.MyAopAdvice : --------------------------------
2020-07-15 15:44:30.674 INFO 20672 --- [ main] c.w.a.MyAopAdvice : 第一个后置返回通知的返回值:true
2020-07-15 15:44:30.675 INFO 20672 --- [ main] c.w.a.MyAopAdvice : 后置最终通知执行了!!!
2020-07-15 15:44:30.722 INFO 20672 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
9、以JUnit方式运行UserControllerTests.testDoAfterThrowingMethod()方法,输出日志如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
2020-07-15 15:46:09.442 INFO 22348 --- [ main] c.w.s.t.UserServiceTests : Starting UserServiceTests on zhenglibing-pc with PID 22348 (started by zheng in E:\sts-workspace\demo)
2020-07-15 15:46:09.451 INFO 22348 --- [ main] c.w.s.t.UserServiceTests : No active profile set, falling back to default profiles: default
2020-07-15 15:46:11.035 INFO 22348 --- [ main] o.s.s.c.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-15 15:46:11.396 INFO 22348 --- [ main] c.w.s.t.UserServiceTests : Started UserServiceTests in 2.235 seconds (JVM running for 3.376)
2020-07-15 15:46:11.620 INFO 22348 --- [ main] c.w.a.MyAopAdvice : 第一个前置通知执行了...
2020-07-15 15:46:11.620 INFO 22348 --- [ main] c.w.a.MyAopAdvice : -------------------------------
2020-07-15 15:46:11.635 INFO 22348 --- [ main] c.w.s.UserService : 正在执行一个业务方法....1
2020-07-15 15:46:11.637 INFO 22348 --- [ main] c.w.a.MyAopAdvice : doAfterThrowingMethod
2020-07-15 15:46:11.637 INFO 22348 --- [ main] c.w.a.MyAopAdvice : 发生了空指针异常!!!
2020-07-15 15:46:11.637 INFO 22348 --- [ main] c.w.a.MyAopAdvice : 后置最终通知执行了!!!
2020-07-15 15:46:11.660 INFO 22348 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
10、以JUnit方式运行UserServiceTests.testDoAroundMethod()方法,日志输出如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
2020-07-15 15:47:35.045 INFO 19712 --- [ main] c.w.s.t.UserServiceTests : Starting UserServiceTests on zhenglibing-pc with PID 19712 (started by zheng in E:\sts-workspace\demo)
2020-07-15 15:47:35.054 INFO 19712 --- [ main] c.w.s.t.UserServiceTests : No active profile set, falling back to default profiles: default
2020-07-15 15:47:36.716 INFO 19712 --- [ main] o.s.s.c.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-15 15:47:37.103 INFO 19712 --- [ main] c.w.s.t.UserServiceTests : Started UserServiceTests in 2.342 seconds (JVM running for 3.479)
2020-07-15 15:47:37.310 INFO 19712 --- [ main] c.w.a.MyAopAdvice : 环绕通知的目标方法名:doAroundMethod
2020-07-15 15:47:37.329 INFO 19712 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
11、以JUnit方式运行UserControllerTests.testHome()方法,日志输出如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
2020-07-15 15:48:23.121 INFO 6900 --- [ main] c.w.c.t.UserControllerTests : Starting UserControllerTests on zhenglibing-pc with PID 6900 (started by zheng in E:\sts-workspace\demo)
2020-07-15 15:48:23.129 INFO 6900 --- [ main] c.w.c.t.UserControllerTests : No active profile set, falling back to default profiles: default
2020-07-15 15:48:25.024 INFO 6900 --- [ main] o.s.b.w.e.t.TomcatWebServer : Tomcat initialized with port(s): 0 (http)
2020-07-15 15:48:25.043 INFO 6900 --- [ main] o.a.c.c.StandardService : Starting service [Tomcat]
2020-07-15 15:48:25.043 INFO 6900 --- [ main] o.a.c.c.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-07-15 15:48:25.141 INFO 6900 --- [ main] o.a.c.c.C.[.[.[/] : Initializing Spring embedded WebApplicationContext
2020-07-15 15:48:25.141 INFO 6900 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1992 ms
2020-07-15 15:48:25.676 INFO 6900 --- [ main] o.s.s.c.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-15 15:48:26.044 INFO 6900 --- [ main] o.s.b.w.e.t.TomcatWebServer : Tomcat started on port(s): 26652 (http) with context path ''
2020-07-15 15:48:26.052 INFO 6900 --- [ main] c.w.c.t.UserControllerTests : Started UserControllerTests in 3.252 seconds (JVM running for 4.383)
2020-07-15 15:48:26.357 INFO 6900 --- [o-auto-1-exec-1] o.a.c.c.C.[.[.[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-07-15 15:48:26.357 INFO 6900 --- [o-auto-1-exec-1] o.s.w.s.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-07-15 15:48:26.386 INFO 6900 --- [o-auto-1-exec-1] o.s.w.s.DispatcherServlet : Completed initialization in 29 ms
2020-07-15 15:48:26.412 INFO 6900 --- [o-auto-1-exec-1] c.w.a.MyAopAdvice : 第一个前置通知执行了...
2020-07-15 15:48:26.412 INFO 6900 --- [o-auto-1-exec-1] c.w.a.MyAopAdvice : -------------------------------
2020-07-15 15:48:26.427 INFO 6900 --- [o-auto-1-exec-1] c.w.s.UserService : 正在执行用户登录方法...
2020-07-15 15:48:26.428 INFO 6900 --- [o-auto-1-exec-1] c.w.a.MyAopAdvice : --------------------------------
2020-07-15 15:48:26.428 INFO 6900 --- [o-auto-1-exec-1] c.w.a.MyAopAdvice : 第一个后置返回通知的返回值:true
2020-07-15 15:48:26.428 INFO 6900 --- [o-auto-1-exec-1] c.w.a.MyAopAdvice : 后置最终通知执行了!!!
2020-07-15 15:48:27.029 INFO 6900 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
通过对各测试运行日志的观察,可以发现返回结果与我们期望的结果相同。
AOP为我们提供了一种以切面方式编程的方法,在业务处理过程中,日志的记录、数据源的切换、事务的处理都可以用AOP的方式解决,而跟Spring Boot的整合,让我们开发AOP又进一步的简化,使面向对象能够更加方便地发挥所长。
案例下载