SpringBoot2 | 第二十五篇(一):SpringAOP入门

AOP,全称为“面向切面编程”。它是通过 预编译方式运行期动态代理 实现程序功能统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP使用可以概括为10个字:纵向重复代码,横向抽取

AOP的初衷

  • Don’t Repeat Yourself(不要重复自己(不要写重复的代码))
  • Separation Of Concerns(关注点分离)
    • 水平分离:展示层 -> 服务层 -> 持久层
    • 垂直分离:模块划分(订单、库存等)
    • 切面分离:功能性需求 和 非功能性需求

​ 它要解决的问题就是将 非功能性需求功能性需求 中剥离出来,集中管理,从而实现 Don’t Repeat Yourself

AOP的好处

  1. 集中处理某一关注点/横切逻辑
  2. 可以很方便的添加/删除关注点
  3. 侵入性少,增强代码可读性与可维护性

AOP的理解

  1. 是一种编程范式,不是编程语言
  2. 解决特定问题,不能解决所有问题
  3. 是OOP的补充,不是替代

编程范式概览

  1. 面向过程编程(C语言,…)
  2. 面向对象编程(Java,C++,…)
  3. 面向接口编程(Java 开发中常见)
  4. 函数式编程(Java8 引入的函数式编程,…)
  5. 事件驱动编程(GUI 开发中常见)
  6. 面向切面编程(Spring 的 AOP)

AOP的应用场景

  1. 权限控制
  2. 缓存控制
  3. 事务控制
  4. 审计日志
  5. 性能监控
  6. 分布式追踪
  7. 异常处理

主角Spring AOP

​ Spring 框架对 AOP 编程提供了支持,通过使用 Spring 提供的 AOP 功能,可以很方便的进行面向切面的编程,许多不容易用传统 OOP 实现的功能都可以使用 AOP 轻松对付。Spring 的 AOP 支持允许将一些通用任务如安全、事务、日志等进行集中式管理,从而提供了更好的复用。

入门案例

产品管理的服务

要求:对产品的添加和删除操作进行权限控制,只允许管理员操作

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.1.0.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9
  • spring-boot-starter-aop:2.1.0.RELEASE
  • lombok:1.18.2

1、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<!-- 引入 AOP 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2、entity

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

import lombok.Data;
import lombok.experimental.Accessors;

/**
* Product 实体
* @author: Fatal
* @date: 2018/11/8 0008 11:14
*/
@Data
@Accessors(chain = true)
public class Product {

private Long id;
private String name;

}

3、hodler

创建一个holder,假设用户登录用用户信息存储在该对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.fatal.hodler;

/**
* 模拟用户持有者
* @author: Fatal
* @date: 2018/11/8 0008 11:25
*/
public class CurrentUserHolder {

private static final ThreadLocal<String> holder = new ThreadLocal<>();

public static String get() {
return holder.get() == null ? "unknown" : holder.get();
}

public static void set(String user) {
holder.set(user);
}

}

4、security

AdminOnly.java

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 自定义权限注解
* @author: Fatal
* @date: 2018/11/8 0008 11:43
*/
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Target(ElementType.METHOD) // 限定该注解的使用对象为`方法`
public @interface AdminOnly {

}

SecurityAspect.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.fatal.security;

import com.fatal.hodler.CurrentUserHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
* 切面
* @author: Fatal
* @date: 2018/11/8 0008 11:35
*/
@Aspect // 标注该类是切面类
@Component
public class SecurityAspect {

/**
* 切点
* 注意:如果注解和切面不在同一个包,记得要用全类名
*/
@Pointcut("@annotation(AdminOnly)")
public void adminOnly() {
}

/**
* 前置通知
*/
@Before("adminOnly()")
public void check() {
String user = CurrentUserHolder.get();
if (!StringUtils.isEmpty(user)) {
if (!"admin".equals(user)) {
throw new RuntimeException("operation not allow");
}
}
}

}

5、service

IProductService.java

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

import com.fatal.entity.Product;

/**
* @author: Fatal
* @date: 2018/11/8 0008 11:15
*/
public interface IProductService {

void insert(Product product);

void delete(Long id);

}

ProductServiceImpl.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
25
26
27
28
29
30
package com.fatal.serivce.impl;

import com.fatal.entity.Product;
import com.fatal.security.AdminOnly;
import com.fatal.serivce.IProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* Product 服务
* @author: Fatal
* @date: 2018/11/8 0008 11:16
*/
@Slf4j
@Service
public class ProductServiceImpl implements IProductService {

@Override
@AdminOnly // 加上该注解后的方法会走切面逻辑
public void insert(Product product) {
log.info("添加产品成功 -- [{}]", product);
}

@Override
@AdminOnly
public void delete(Long id) {
log.info("删除id为[{}]的产品成功", id);
}

}

6、Test

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.fatal;

import com.fatal.entity.Product;
import com.fatal.hodler.CurrentUserHolder;
import com.fatal.serivce.IProductService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter251ApplicationTests {

@Autowired
private IProductService productService;

/**
* 测试用户没有添加权限
* 执行结果:抛异常 java.lang.RuntimeException: operation not allow
*/
@Test
public void insert() {
CurrentUserHolder.set("tom");
productService.insert(new Product().setId(1l).setName("product"));
}

/**
* 测试用户具有添加权限
* 执行结果:正常删除
*/
@Test
public void delete() {
CurrentUserHolder.set("admin");
productService.delete(1l);
}

}

笔记

1、切面的模板

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Aspect     // 标注该类是切面类
@Component // 必须把切面作为一个组件放到容器中
public class SecurityAspect {

/**
* 切点
*/
@Pointcut("Spring切面表达式")
public void point() {
}

/**
* 前置通知
*/
@Before("point()")
public void before() {
// 在目标方法被调用之前调用通知功能
}

/**
* 后置通知
*/
@After
public void after() {
// 在目标方法完成之后调用通知,无论方法执行成功与否
}

/**
* 返回通知
*/
@AfterReturning
public void afterReturning() {
// 在目标方法成功执行之后调用
}

/**
* 异常通知
*/
@AfterThrowing
public void afterThrowing() {
// 在目标方法抛出异常时调用通知
}

/**
* 环绕通知
*/
@Around
public void around() {
// 环绕通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
}

}

2、Spring切面表达式

​ 案例中 @annotation(AdminOnly) 为切点表达式。表示对方法上标注有 @AdminOnly 注解的方法进行拦截。下一章将会对 Spring切面表达式 进行系统的介绍

参考资料

探秘Spring AOP

总结

SpringBoot 自动开启 AOP 功能了,我们可以直接使用 @Transactional 、@Async 或者自定义的基于AOP 的注解实现切面编程

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter25_1

学习 apollo_0001 前辈的经验