您现在的位置是:首页 >技术交流 >SpringBoot AOP切面编程 使用案例网站首页技术交流
SpringBoot AOP切面编程 使用案例
简介SpringBoot AOP切面编程 使用案例
参考资料
- Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
- 【SpringBoot-3】切面AOP实现权限校验:实例演示与注解全解
- 【小家Spring】Spring AOP中@Pointcut切入点表达式最全面使用介绍
- AOP编程过程中的Signature接口
本篇文章核心思想均摘自参考资料所示的博客,详情请参阅其博客。
一. 知识储备
1.1 图示
1.2 图示解释
Pointcut
:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。
切点分为execution方式和annotation方式。execution
可以用路径表达式指定哪些类织入切面annotation
可以指定被哪些注解修饰的代码织入切面。
Advice
:处理,包括处理时机和处理内容。- 处理内容就是要做什么事,比如校验权限和记录日志。
- 处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
Aspect
:切面,即Pointcut和Advice。Joint point
:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。Weaving
:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
二. 前期准备1
2.1 POM文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 自定义注解
- 该自定义注解作用于实体类的属性上
- 自定义注解和接口类似,可以在接口中定义枚举类
- 该注解用来标记属性值被自动添加为当前Date或者登录用户ID
import java.lang.annotation.*;
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldValueAnnotation {
Category type();
// 自定义注解中定义的枚举类
enum Category {
dateField,
userIdField,
}
}
2.3 form基类
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@Data
public class BaseForm {
// 页面ID
private String pageId;
// 登录用户ID
@FieldValueAnnotation(type = FieldValueAnnotation.Category.userIdField)
private String loginUserId;
// 创建日期
// 返回前台json数据的时候,忽略此字段
@JsonIgnore
@FieldValueAnnotation(type = FieldValueAnnotation.Category.dateField)
private Date createDate;
// 更新日期
@JsonIgnore
@FieldValueAnnotation(type = FieldValueAnnotation.Category.dateField)
private Date updateDate;
// 语言code
private String languageCode;
// 画面项目Map
private Map<String, String> itemMap;
}
2.4 提供业务数据
import lombok.Builder;
import lombok.Data;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class CommonService {
// 支持的所有语言列表
public List<LanguageEntity> getLanguageList() {
return Arrays.asList(
LanguageEntity.builder().code("jp").language("日语").build(),
LanguageEntity.builder().code("zh").language("中文").build(),
LanguageEntity.builder().code("en").language("英文").build()
);
}
// 系统支持语言实体类
@Data
@Builder
public static class LanguageEntity {
private String code;
private String language;
}
// 模拟数据库查询到的数据
private Map<String, Map<String, Map<String, String>>> getItemMap() {
// j002页面的国际化数据
Map<String, String> zhMapJ002 = new HashMap<>() {{
put("name", "姓名");
put("age", "年龄");
put("language", "语言");
put("btnName", "提交");
}};
Map<String, String> jpMapJ002 = new HashMap<>() {{
put("name", "名前");
put("age", "年齢");
put("language", "言語");
put("btnName", "コミット");
}};
Map<String, String> enMapJ002 = new HashMap<>() {{
put("name", "name");
put("age", "age");
put("language", "language");
put("btnName", "commit");
}};
Map<String, Map<String, String>> j002Map = new HashMap<>() {{
put("zh", zhMapJ002);
put("jp", jpMapJ002);
put("en", enMapJ002);
}};
// j003页面的国际化数据
Map<String, String> zhMapJ003 = new HashMap<>() {{
put("fruit", "苹果");
put("drink", "可乐");
put("language", "语言");
}};
Map<String, String> jpMapJ003 = new HashMap<>() {{
put("fruit", "リンゴ");
put("drink", "コーラ");
put("language", "言語");
}};
Map<String, String> enMapJ003 = new HashMap<>() {{
put("fruit", "apple");
put("drink", "cola");
put("language", "language");
}};
Map<String, Map<String, String>> j003Map = new HashMap<>() {{
put("zh", zhMapJ003);
put("jp", jpMapJ003);
put("en", enMapJ003);
}};
// 最终的数据
return new HashMap<>() {{
put("j002", j002Map);
put("j003", j003Map);
}};
}
// 根据地区和画面id获取画面项目
public Map<String, String> getItemByLocal(String local, String pageId) {
return this.getItemMap().get(pageId).get(local);
}
// 模拟数据库获取下拉列表内容
private Map<String, List<SelectItem>> getSelectList() {
List<SelectItem> zhSelectItems = Arrays.asList(
SelectItem.builder().code("c001").name("汽车").build(),
SelectItem.builder().code("c002").name("电脑").build(),
SelectItem.builder().code("c003").name("手机").build()
);
List<SelectItem> jpSelectItems = Arrays.asList(
SelectItem.builder().code("c001").name("車").build(),
SelectItem.builder().code("c002").name("パソコン").build(),
SelectItem.builder().code("c003").name("スマホ").build()
);
List<SelectItem> enSelectItems = Arrays.asList(
SelectItem.builder().code("c001").name("car").build(),
SelectItem.builder().code("c002").name("computer").build(),
SelectItem.builder().code("c003").name("phone").build()
);
return new HashMap<>(){{
put("zh", zhSelectItems);
put("jp", jpSelectItems);
put("en", enSelectItems);
}};
}
// 根据画面local地区,获取下列列表内容
public List<SelectItem> getSelectListByLocal(String local) {
if (ObjectUtils.isEmpty(local)) {
local = "jp";
}
return this.getSelectList().get(local);
}
// 画面下拉列表实体类
@Data
@Builder
public static class SelectItem {
private String code;
private String name;
}
}
2.5 封装返回前台数据的实体类
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class ResultEntity implements Serializable {
private Boolean result;
private Object entity;
private ResultEntity(Boolean result, Object entity) {
this.result = result;
this.entity = entity;
}
public static ResultEntity ok(Object entity) {
return new ResultEntity(true, entity);
}
}
三. AOP实现页面国际化 + 共通属性值自动封装案例
3.1 页面
⏹登录页面 ⇒ j001.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>登录页面!!!</h1>
<button id="btn">点击跳转到国际化页面</button>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>
$("#btn").click(() => {
window.location = "/j002/init";
});
</script>
</html>
⏹登录之后显示的国际化页面 ⇒ j002.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<label>
<span id="language">[[${itemMap.language}]]</span>
<select id="langList">
<th:block th:each="content : ${langList}">
<option th:value="${content.code}">[[${content.language}]]</option>
</th:block>
</select>
</label>
<hr>
<span id="name">[[${itemMap["name"]}]]</span><br>
<span id="age">[[${itemMap["age"]}]]</span>
<hr>
<select id="selectList" th:object="${entity}">
<th:block th:each="selectEntity : *{selectList}">
<option th:value="${selectEntity.code}">[[${selectEntity.name}]]</option>
</th:block>
</select>
<button id="btnName">[[${itemMap["btnName"]}]]</button>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script th:inline="javascript">
// 打印当前页面的项目信息
const itemInfo = [[${itemMap}]];
console.log(itemInfo);
// 打印当前画面的id
const pageId = /*[[${pageId}]]*/ '';
console.log(pageId);
</script>
<script>
$(function() {
eventBind();
});
function eventBind() {
// 当语言下拉列表切换语言的时候,修改页面上的语言项目和下拉列表项目
$("#langList").change(function({target: {value}}) {
const data = {
languageCode: value,
pageId,
};
$.ajax({
url: `/j002/languageChange`,
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json;charset=utf-8',
success: function (data, status, xhr) {
setViewItem(data);
setSelectList(data);
}
});
});
// 提交数据到后台
$("#btnName").click(function (event) {
$.ajax({
url: `/j002/dataToDB`,
type: 'POST',
data: JSON.stringify({pageId}),
contentType: 'application/json;charset=utf-8',
success: function (data, status, xhr) {
console.log(data);
}
});
});
}
// 选中画面上的所有项目
function setViewItem({entity: {itemMap}}) {
for (const [key, value] of Object.entries(itemMap)) {
$(`#${key}`).text(value);
}
}
// 设置下拉列表选纵横
function setSelectList({entity: {selectList}}) {
const selectObj = $("#selectList");
const selectedCode = selectObj.val();
selectObj.empty();
for (const entity of selectList) {
$("<option>", {
value: entity.code,
text: entity.name
}).appendTo("#selectList");
}
selectObj.val(selectedCode);
}
</script>
</html>
3.2 Controller层
⏹ 登录页面Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpSession;
import java.util.List;
@Controller
@RequestMapping("/j001")
public class J001LoginController {
@Autowired
private HttpSession session;
@Autowired
private CommonService commonService;
@GetMapping("/init")
public ModelAndView init() {
// 获取整个系统支持的语言下列列表放到session中
List<CommonService.LanguageEntity> languageEntityList = commonService.getLanguageList();
session.setAttribute("_langList", languageEntityList);
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("j001");
return modelAndView;
}
}
⏹ 国际化显示页面Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/j002")
public class J002BusinessController {
@Autowired
private CommonService commonService;
@Autowired
private J002BusinessService service;
@GetMapping("/init")
public ModelAndView init(J002Form form) {
// 将下拉列表放到前台
J002Entity entity = new J002Entity();
entity.setSelectList(commonService.getSelectListByLocal(form.getLanguageCode()));
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("j002");
modelAndView.addObject("testMsg", "测试消息");
modelAndView.addObject("entity", entity);
return modelAndView;
}
// 语言切换的时候,去数据库查询下拉列表的国际化数据,然后放到前台
@PostMapping("/languageChange")
@ResponseBody
public ResultEntity languageChange(@RequestBody J002Form form) {
List<CommonService.SelectItem> selectListByLocal = commonService.getSelectListByLocal(form.getLanguageCode());
J002Entity entity = new J002Entity();
entity.setSelectList(selectListByLocal);
return ResultEntity.ok(entity);
}
// 插入数据到数据库
@PostMapping("/dataToDB")
@ResponseBody
public void dataToDB(@RequestBody J002Form form) {
service.insertDataToDB(form);
service.updateDataToDB(form);
}
}
3.3 Service层
import org.springframework.stereotype.Service;
@Service
public class J002BusinessService {
public void insertDataToDB(J002Form form) {
System.out.println(form);
}
public void updateDataToDB(J002Form form) {
System.out.println(form);
}
}
3.4 核心
的切面编程类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.Map;
@Aspect
@Component
public class ControllerAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private HttpSession session;
@Autowired
private CommonService commonService;
// 模拟当前的登录用户
private final static String loginUserId = "jmw";
// 只有一层包,所以是 controller.* ,如果有两层包的话,就是 controller.*.*
@Before("execution(* com.example.jmw.aspect.controller.*.init(..)) && args(form)")
public void beforeInit(JoinPoint join, BaseForm form) {
// 获取当前画面的ID
String uri = request.getRequestURI().substring(request.getContextPath().length());
Path uriPath = Paths.get(uri);
String pageId = uriPath.subpath(0, 1).toString();
// 将当前的画面ID放到form中
form.setPageId(pageId);
}
// 在ModelAndView返回到页面之前,向其中加入页面上的项目
@AfterReturning(value = "execution(* com.example.jmw.aspect.controller.*.init(..)) && args(form)", argNames = "join,form,modelAndView", returning = "modelAndView")
public void afterReturningInit(JoinPoint join, BaseForm form, ModelAndView modelAndView) {
// 获取画面ID
String pageId = form.getPageId();
// 根据画面ID查询当前页面的国际化项目名称
Map<String, String> itemMap = commonService.getItemByLocal("jp", pageId);
modelAndView.addObject("itemMap", itemMap);
// 将当前页面的语言list传到前台
modelAndView.addObject("langList", session.getAttribute("_langList"));
// 将当前页面的pageId传到前台
modelAndView.addObject("pageId", pageId);
}
@AfterReturning(value = "execution(* com.example.jmw.aspect.controller.*.languageChange(..)) && args(form, ..)", argNames = "join,form,resultEntity", returning = "resultEntity")
public void postLanguageChange(JoinPoint join, BaseForm form, ResultEntity resultEntity) {
// 如果不是成功的响应就return
if (!resultEntity.getResult()) {
return;
}
// 因为每个画面用到的实体类都不相同,因此此处将每个画面的实体类获取出来之后,转换为父类的BaseForm,每个画面共通的字段都放到此处
BaseForm baseForm = (BaseForm)resultEntity.getEntity();
if (ObjectUtils.isEmpty(baseForm)) {
baseForm = new BaseForm();
}
// 根据画面ID查询当前页面的国际化项目名称
Map<String, String> itemMap = commonService.getItemByLocal(form.getLanguageCode(), form.getPageId());
// 将国际化项目名称放到BaseForm中,也就相当于放到了每个画面自己的实体类中
baseForm.setItemMap(itemMap);
}
// 切面service包下面所有的Service类里面所有以 insert 或者 update 开头的方法,并且该方法一定要有一个参数为 form
@Before("execution(* com.example.jmw.aspect.service.*.insert*(..)) && args(form) " +
// 可以单独定义一个类,类里面用来聚合所有的 切面表达式
"|| com.example.jmw.common.aspect.ControllerAspect.PointcutClass.updateAspect(form)")
public void beforeInsertOrUpdate(JoinPoint join, BaseForm form) throws Exception {
// 获取出切面对象
Signature signature = join.getSignature();
this.getSignatureInfo(signature);
// 获取出切面对象的所有参数对象
Object[] args = join.getArgs();
// 获取出第一个参数,并转换为父类(获取出共通的属性)
Object objForm = args[0];
// 通过反射来设置属性
this.setFields(objForm);
// 将子类强转换为共通的基类,基类中有我们要转换的共通的属性
BaseForm argForm = (BaseForm)objForm;
// 可以看到通过JoinPoint得到的参数对象和beforeInsertOrUpdate方法中的参数对象本质上是一个对象
System.out.println(argForm == form); // true
// 通过反射获取出设置的loginUserId
String loginUserId = (String)objForm.getClass().getField("loginUserId").get(objForm);
// 给loginUserId添加后缀,通过这种方式设置form中的值更简洁,但是需要注意抽取基类和共通属性
form.setLoginUserId(loginUserId + "~我是一个后缀~");
}
// 设置Object对象中的属性
private void setFields(Object objectForm) throws IllegalAccessException {
// 获取当前类的class对象
Class<?> objClass = objectForm.getClass();
// 获取当前类的父类的class对象
Class<?> superclass = objClass.getSuperclass();
// 获取父类上声明的所有类型的属性
Field[] fields = superclass.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(FieldValueAnnotation.class)) {
continue;
}
// 获取field属性上标记的注解
FieldValueAnnotation fieldAnnotation = field.getAnnotation(FieldValueAnnotation.class);
FieldValueAnnotation.Category fieldTypeEnum = fieldAnnotation.type();
// 获取注解上标记的类型名称
String name = fieldTypeEnum.name();
// 因为属性是私有属性,通过setAccessible()将其设置为允许访问
field.setAccessible(true);
// 根据类型设置不同的值
if (name.equals(FieldValueAnnotation.Category.userIdField.name())) {
field.set(objectForm, loginUserId);
} else if (name.equals(FieldValueAnnotation.Category.dateField.name())) {
field.set(objectForm, new Date());
}
}
}
// 可以将切面表达式聚合到一个类里面集中管理
public class PointcutClass {
@Pointcut("execution(* com.example.jmw.aspect.service.*.update*(..)) && args(form, ..)")
private void updateAspect(BaseForm form){}
}
// 获取切面Signature中的信息
private void getSignatureInfo(Signature signature) {
// 返回此签名的标识符部分。对于方法,这将返回方法名称。
String name = signature.getName();
System.out.println(name); // insertDataToDB
// 获取被切面的对象的短名称
String shortString = signature.toShortString();
System.out.println(shortString); // J002BusinessService.insertDataToDB(..)
// 获取被切面的对象的长名称
String longString = signature.toLongString();
System.out.println(longString); // public void com.example.jmw.aspect.service.J002BusinessService.insertDataToDB(com.example.jmw.aspect.form.J002Form)
// 获取被切面方法切到的方法的修饰符
int modifiers = signature.getModifiers();
String modifierName = Modifier.toString(modifiers);
System.out.println(modifierName); // public
Class declaringType = signature.getDeclaringType();
System.out.println(declaringType); // class com.example.jmw.aspect.service.J002BusinessService
String declaringTypeName = signature.getDeclaringTypeName();
System.out.println(declaringTypeName); // com.example.jmw.aspect.service.J002BusinessService
}
}
四. 前期准备2
4.1 自定义注解
4.1.1 标记方法需要加密
的注解
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptInfoMethodAnnotation {
}
4.1.2 标记方法需要解密
的注解
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptInfoMethodAnnotation {
}
4.1.3 标记属性需要加密或者解密
的注解
import java.lang.annotation.*;
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptFieldAnnotation {
}
五. 敏感数据加密和解密案例
5.1 前台
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<button id="insert">插入数据</button>
<button id="select">查询数据</button>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>
$("#insert").click(function() {
const data = {
id: "011",
name: "贾飞天",
password: "pass001",
email: "123@qq.com"
};
$.ajax({
url: `/j003/insert`,
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json;charset=utf-8',
success: function (data, status, xhr) {
console.log(data);
}
});
});
$("#select").click(function() {
$.ajax({
// 注意,此处的查询参数少了一个0
url: `/j003/select?id=11`,
type: 'GET',
success: function (data, status, xhr) {
console.log(data);
}
});
});
</script>
</html>
5.2 前台Form
import com.example.jmw.common.annotation.aspect.EncryptDecryptFieldAnnotation;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class J003Form extends BaseForm {
private String id;
private String name;
// 需要加密的字段
@EncryptDecryptFieldAnnotation
private String password;
// 需要加密的字段
@EncryptDecryptFieldAnnotation
private String email;
}
5.3 返回数据实体类
- 父类中的J003Form已经有了
password
和email
字段 - 子类继承父类,并定义
password
和email
字段来重写父类属性
import com.example.jmw.common.annotation.aspect.EncryptDecryptFieldAnnotation;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class J003Entity extends J003Form {
// 需要解密的字段
@EncryptDecryptFieldAnnotation
private String password;
// 需要解密的字段
@EncryptDecryptFieldAnnotation
private String email;
}
5.4 Mapper层
⏹接口
public interface J003Mapper {
void insertUserInfo(J003Form form);
J003Entity selectUserInfo(String id);
}
⏹SQL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.jmw.mapper.J003Mapper">
<select id="insertUserInfo" parameterType="com.example.jmw.aspect.form.J003Form">
INSERT INTO
aspect_user(
id
, name
, password
, email
)
VALUES (
#{id}
, #{name}
, #{password}
, #{email}
)
</select>
<select id="selectUserInfo" parameterType="string" resultType="com.example.jmw.aspect.entity.J003Entity">
SELECT
id
, name
, password
, email
FROM
aspect_user
WHERE
id = #{id}
</select>
</mapper>
5.5 Controller层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/j003")
public class J003BusinessController {
@Autowired
private J003BusinessService service;
@GetMapping("/init")
public ModelAndView init() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("j003");
return modelAndView;
}
@PostMapping("/insert")
@ResponseBody
public void insertUserInfo(@RequestBody J003Form form) {
service.insertUserInfo(form);
}
@GetMapping("/select")
@ResponseBody
public J003Entity selectUserInfo(@RequestParam String id) {
J003Entity entity = service.selectUserInfoTest(id, "");
System.out.println(entity);
J003Entity j003Entity = service.selectUserInfo(id);
return j003Entity;
}
}
5.6 Service层
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class J003BusinessService {
@Resource
private J003Mapper mapper;
// 给信息加密的注解
@EncryptInfoMethodAnnotation
public void insertUserInfo(J003Form form) {
mapper.insertUserInfo(form);
}
// 给信息解密的注解
@DecryptInfoMethodAnnotation
public J003Entity selectUserInfo(String id) {
return mapper.selectUserInfo(id);
}
// 给信息解密的注解
@DecryptInfoMethodAnnotation
public J003Entity selectUserInfoTest(String id, String blank) {
return mapper.selectUserInfo(id);
}
}
5.7 Base64加密解密工具类
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public final class Base64Utils {
private final static Base64.Encoder encoder = Base64.getEncoder();
private final static Base64.Decoder decoder = Base64.getDecoder();
// 加密
public static String encode(String text) {
return encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8));
}
// 解密
public static String decode(String encodedText) {
return new String(decoder.decode(encodedText), StandardCharsets.UTF_8);
}
}
5.8 核心
的切面编程类
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import static java.util.stream.Collectors.toList;
@Aspect
@Component
public class EncryptAndDecryptAspect {
/*
功能: 获取方法上的待加密对象
切面内容: 方法上被标记了 EncryptInfoMethodAnnotation 注解
*/
@Before("@annotation(com.example.jmw.common.annotation.aspect.EncryptInfoMethodAnnotation)")
public void infoEncrypt(JoinPoint joinPoint) throws Exception {
Object form = joinPoint.getArgs()[0];
Class<?> formClass = form.getClass();
Field[] declaredFields = formClass.getDeclaredFields();
for (Field field : declaredFields) {
if (!field.isAnnotationPresent(EncryptDecryptFieldAnnotation.class) || field.getType() != String.class) {
continue;
}
// 强制private属性可访问
field.setAccessible(true);
// 属性值进行加密
String encodeValue = Base64Utils.encode((String) field.get(form));
// 加密后的属性值塞到原属性中
field.set(form, encodeValue);
}
}
/*
功能: 获取方法上的待解密对象,这种写法还能获取方法上标记的注解
切面内容: 方法上被标记了 DecryptInfoMethodAnnotation 注解,并且方法上需要有一个参数
注意:如果方法上标记了 DecryptInfoMethodAnnotation 注解,并且方法没有参数或者多个参数的话,不会被切面到
@Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
@Around功能虽然强大,但通常需要在线程安全的环境下使用。
因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。
此处使用 @Around 仅为测试其作用
*/
@Around("@annotation(decryptInfoMethodAnnotation) && args(id)")
public Object infoDecrypt(ProceedingJoinPoint joinPoint, DecryptInfoMethodAnnotation decryptInfoMethodAnnotation, String id) throws Throwable {
System.out.println(decryptInfoMethodAnnotation);
System.out.println(id); // 11
// 对参数进行处理,如果不满3位就补0
String leftPadStr = StringUtils.leftPad(id, 3, "0");
Object[] args = {
leftPadStr
};
/*
执行切面切到的方法,获取到方法执行的返回值
如果使用的是 joinPoint.proceed() 的话,则相当于不改变目标方法的参数值
我们使用的是 joinPoint.proceed(args) , 系统 args 是我们改变了参数值之后的参数数组
如果传入的Object[]数组长度与目标方法所需要的参数个数不相等, 或者Object[]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
*/
Object resultObj = joinPoint.proceed(args);
if (ObjectUtils.isEmpty(resultObj)) {
return resultObj;
}
// 过滤出标记了 @EncryptDecryptFieldAnnotation 注解的属性
Class<?> resultObjClass = resultObj.getClass();
List<Field> fieldList = Arrays.stream(resultObjClass.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EncryptDecryptFieldAnnotation.class)).collect(toList());
if (ObjectUtils.isEmpty(fieldList)) {
return resultObj;
}
for (Field field : fieldList) {
// 强制private属性可访问
field.setAccessible(true);
// 对加密数据进行解密并设置到原属性中
String encryptFieldValue = (String)field.get(resultObj);
String decodeFieldValue = Base64Utils.decode(encryptFieldValue);
field.set(resultObj, decodeFieldValue);
}
return resultObj;
}
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。