在上一篇《关于日志打印的几点建议以及非最佳实践》的末尾提到了日志打印更为高级的一种方式——利用Spring AOP。在打印日志时,通常都会在业务逻辑代码中插入日志打印的语句,这实际上是和业务无关的代码,这就带来了较强的侵入性编码。较为理想的编码方式,日志和业务代码应该是分离的。
利用Spring AOP就能很好的实现这种业务分离。AOP并不是Spring所特有的,它的全称是Aspect-Oriented Programming(面向切面编程),切面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(维基百科)。看不懂定义没关系,要知道它和某种特定的语言无关,和OOP(面向对象编程)类似。但它和OOP并不是相互替代的关系,AOP并不是比OOP更高级,它不是用来替代OOP的,反之它是在某些特定领域用于辅佐OOP的。所以要清楚并不是所有的场景都适用AOP,它有它的适用场景。
那么首先需要知道的是什么是它可以使用的场景。
假设上面的长方形是一个完整的业务,可是我们要在其中添加一行日志代码,这行日志代码就“破坏”了这个完整的业务,就好像是在这个业务中间切了一刀。当然不止是日志能“破坏”、“切断”这个业务,还有事务、权限控制等,都能像一把刀一样切掉这个完整的业务模型,带来碍眼的侵入式编程。日志称之为横切关注点,日志的这个类集中在代码的一个地方叫做切面,之所以强调集中在代码的一个地方,是因为像以前侵入式编程日志这种横切关注点是散落在系统的各个地方。故,侵入式编程中也有横切关注点概念,横切关注点表示散落在程序各个地方的功能;但,切面只有在AOP中才有,那是横切关注点不再侵入式的散落在程序各个地方而是集中起来被模块化。 接着我们一一理解AOP中的术语。
在上文我们将横切关注点集中起来管理,它不再散落在程序的各个地方,而是被模块化,称之为切面。那么定义横切关注点在何时工作(这并不完全准确,不仅是何时工作,也包括具体的工作是什么),在某个的调用前还是调用后还是抛出异常时?定义在何时工作以及工作内容称之为通知,Spring中的切面一共提供5种通知的类型:
前置通知(Before)
后置通知(After)
返回通知(After-Running)
异常通知(After-throwing)
环绕通知(Around)
前面4个较为容易理解,例如“前置通知”,我们通常在一个方法的第一句打印出传入的方法参数,此时就可以使用前置通知在方法调用前打印出传入的参数。对于“后置通知”实际是“返回通知”和“异常通知”的并集,返回通知表示程序正确运行返回后执行,异常通知表示程序不正常运行抛出异常时执行,而后置通知则不论程序是否正确运行,一旦离开方法就会执行。
环绕通知最为强大,它包裹了被通知的方法,可同时定义前置通知和后置通知。
通知定义了何时工作以及工作内容,切点则定义了在何处工作,也就是在哪个方法应用通知。要表达出在哪个方法中运用通知,这需要用到切点表达式。Spring AOP借助AspectJ(另一种AOP实现)的切点表达式来确定通知被应用的位置,虽然是借助但并不支持所有AspectJ的所有切点指示器而仅仅是其一个子集,这其中最为常用的就是execution切点指示器,表示执行。例如:
execution(* com.deo.springaop.Test.test(..))
更多的切点指示器使用时查阅即可。
上面介绍了AOP中最为基本的两个术语,通知和切点。简单总结下,横切关注点集中在了一个地方被模块化称之为切面,通知和切点构成了切面的所有内容——它是什么,在何时和何处完成其功能。
在对AOP作了简要介绍后,接下来简单使用一下Spring AOP。例子源于慕课网的一节课程,对其稍作修改,课程地址http://www.imooc.com/video/15699,如有侵权,联系删除。例子的完整代码
我们模拟用户删除的一个删除操作,此操作需要由管理员“admin”才能删除,其余用于不可删除。这是一个权限访问的问题,在不适用AOP的情况下,通常会在删除方法前用户作权限的校验,例如:
1 public void delete(long id) {
2 authService.checkAccess();
3 //TODO:do something.
4 }
显然,第2行代码是与删除逻辑不相干的代码,也就是说这是典型的侵入式编程。在学习AOP后我们可以通过面向切面编程,将这种散落在程序中的代码剥离出来,使之不与业务逻辑相耦合。
首先创建一个Maven工程,其pom.xml配置的依赖如下所示:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.7.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
</dependencies>
因为Spring AOP使用了AspectJ相关的东西,所以需要引入AspectJ包。接着创建如下图所示的包结构:
配置applicationContext.xml:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xmlns:context="http://www.springframework.org/schema/context"
5 xmlns:aop="http://www.springframework.org/schema/aop"
6 xsi:schemaLocation="http://www.springframework.org/schema/beans
7 http://www.springframework.org/schema/beans/spring-beans.xsd
8 http://www.springframework.org/schema/context
9 http://www.springframework.org/schema/context/spring-context.xsd
10 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
11
12 <!--扫描指定包下的类,将其注册为bean-->
13 <context:component-scan base-package="com.springdemo.aop"/>
14 <!--启用AspectJ自动代理,其中proxy-target-class为true表示使用CGLib的代理方式,false表示JDK的代理方式,默认false-->
15 <aop:aspectj-autoproxy />
16 </beans>
其实使用xml的配置方式是比较繁琐的,我们可以使用JavaConfig的配置方式,当然也可以使用基于Spring Boot的无配置方式。 接下来先写业务相关的类,也就是ProductService,删除产品的服务类,我们只写业务相关的逻辑,不再加入权限校验。
1 package com.springdemo.aop.service;
2
3 import org.springframework.stereotype.Service;
4
5 @Service
6 public class ProductService {
7
8 public void delete(long id) {
9 System.out.println("delete product");
10 }
11 }
当然不再加入权限校验的代码不代表不需要权限的校验,实际上权限校验的逻辑如下:
1 package com.springdemo.aop.service;
2
3 import com.springdemo.aop.security.CheckUserHolder;
4 import org.springframework.stereotype.Component;
5
6 @Component
7 public class AuthService {
8 public void checkAccess() {
9 String user = CheckUserHolder.get();
10 if (!"admin".equals(user)) {
11 throw new RuntimeException("权限不够");
12 }
13 }
14 }
CheckUserHolder类是我们模拟的上下文,用于获取用户名。
1 package com.springdemo.aop.security;
2
3 public class CheckUserHolder {
4 public static final ThreadLocal<String> holder = new ThreadLocal<String>();
5
6 public static String get() {
7 return holder.get() == null ? "unknow" : holder.get();
8 }
9
10 public static void set(String user) {
11 holder.set(user);
12 }
13 }
最后就是重点,定义一个切面。上面说到,切面由通知和切点组成,现在在权限校验的此例中,我们需要在删除前就判断用户是否有权限,也就是“前置通知”——Before,而我们需要匹配ProductService类中的delete方法,所以切点也就是在delete方法。
1 package com.springdemo.aop.security;
2
3 import com.springdemo.aop.service.AuthService;
4 import org.aspectj.lang.annotation.Aspect;
5 import org.aspectj.lang.annotation.Before;
6 import org.aspectj.lang.annotation.Pointcut;
7 import org.springframework.beans.factory.annotation.Autowired;
8 import org.springframework.stereotype.Component;
9
10 @Aspect
11 @Component
12 public class SecurityAspect {
13
14 @Autowired
15 private AuthService authService;
16
17 @Pointcut("execution(* com.springdemo.aop.service.ProductService.delete(..))")
18 public void adminOnly(){}
19
20 @Before("adminOnly()")
21 public void check() {
22 authService.checkAccess();
23 }
24 }
@Pointcut是切点的意思,定义了一个切点,我们在后面的通知就能像Before那样使用。更为高级的用法是将切点表达式定义为一个注解,这样我们就能在我们需要通知的方法前加入注解就可以了,这里对这种较为高级的方式不做介绍。 这样我们就完成的代码的边写,接下来是单元测试:
1 package com.springdemo.aop;
2
3 import com.springdemo.aop.security.CheckUserHolder;
4 import com.springdemo.aop.service.ProductService;
5 import org.junit.Test;
6 import org.junit.runner.RunWith;
7 import org.springframework.beans.factory.annotation.Autowired;
8 import org.springframework.test.context.ContextConfiguration;
9 import org.springframework.test.context.junit4.SpringRunner;
10
11 @RunWith(SpringRunner.class)
12 @ContextConfiguration(locations = {"classpath*:applicationContext.xml"})
13 public class AopTest {
14
15 @Autowired
16 private ProductService productService;
17
18 /**
19 * 匿名权限访问校验
20 */
21 @Test(expected = Exception.class) //正确结果应该抛出异常
22 public void annoDeleteTest() {
23 CheckUserHolder.set("kevin");
24 productService.delete(1L);
25 }
26
27 /**
28 * 管理员权限校验
29 */
30 @Test
31 public void adminDelete() {
32 CheckUserHolder.set("admin");
33 productService.delete(1L);
34 }
35 }
通过。
本篇Spring AOP初级入门就介绍到这里,下一篇我们将更为完整和真实的模拟常见的Spring AOP使用场景。