参考资料
- Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
- 【SpringBoot-3】切面AOP实现权限校验:实例演示与注解全解
- 【小家Spring】Spring AOP中@Pointcut切入点表达式最全面使用介绍
- AOP编程过程中的Signature接口
本篇文章核心思想均摘自参考资料所示的博客,详情请参阅其博客。
目录
- 一. 知识储备
- 1.1 图示
- 1.2 图示解释
- 二. 前期准备1
- 2.1 POM文件
- 2.2 自定义注解
- 2.3 form基类
- 2.4 提供业务数据
- 2.5 封装返回前台数据的实体类
- 三. AOP实现页面国际化 + 共通属性值自动封装案例
- 3.1 页面
- 3.2 Controller层
- 3.3 Service层
- 3.4 `核心`的切面编程类
- 四. 前期准备2
- 4.1 自定义注解
- 4.1.1 标记方法需要`加密`的注解
- 4.1.2 标记方法需要`解密`的注解
- 4.1.3 标记属性需要`加密或者解密`的注解
- 五. 敏感数据加密和解密案例
- 5.1 前台
- 5.2 前台Form
- 5.3 返回数据实体类
- 5.4 Mapper层
- 5.5 Controller层
- 5.6 Service层
- 5.7 Base64加密解密工具类
- 5.8 `核心`的切面编程类
一. 知识储备
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 {// 页面IDprivate 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;// 语言codeprivate String languageCode;// 画面项目Mapprivate 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@Builderpublic 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@Builderpublic 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);// 打印当前画面的idconst 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 {@Autowiredprivate HttpSession session;@Autowiredprivate 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 {@Autowiredprivate CommonService commonService;@Autowiredprivate 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")@ResponseBodypublic 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")@ResponseBodypublic 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 {@Autowiredprivate HttpServletRequest request;@Autowiredprivate HttpSession session;@Autowiredprivate 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) {// 获取当前画面的IDString 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) {// 获取画面IDString 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) {// 如果不是成功的响应就returnif (!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// 通过反射获取出设置的loginUserIdString 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); // publicClass declaringType = signature.getDeclaringType();System.out.println(declaringType); // class com.example.jmw.aspect.service.J002BusinessServiceString 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({// 注意,此处的查询参数少了一个0url: `/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;// 需要加密的字段@EncryptDecryptFieldAnnotationprivate String password;// 需要加密的字段@EncryptDecryptFieldAnnotationprivate 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 {// 需要解密的字段@EncryptDecryptFieldAnnotationprivate String password;// 需要解密的字段@EncryptDecryptFieldAnnotationprivate 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 INTOaspect_user(id, name, password, email)VALUES (#{id}, #{name}, #{password}, #{email})</select><select id="selectUserInfo" parameterType="string" resultType="com.example.jmw.aspect.entity.J003Entity">SELECTid, name, password, emailFROMaspect_userWHEREid = #{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 {@Autowiredprivate J003BusinessService service;@GetMapping("/init")public ModelAndView init() {ModelAndView modelAndView = new ModelAndView();modelAndView.setViewName("j003");return modelAndView;}@PostMapping("/insert")@ResponseBodypublic void insertUserInfo(@RequestBody J003Form form) {service.insertUserInfo(form);}@GetMapping("/select")@ResponseBodypublic 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 {@Resourceprivate J003Mapper mapper;// 给信息加密的注解@EncryptInfoMethodAnnotationpublic void insertUserInfo(J003Form form) {mapper.insertUserInfo(form);}// 给信息解密的注解@DecryptInfoMethodAnnotationpublic J003Entity selectUserInfo(String id) {return mapper.selectUserInfo(id);}// 给信息解密的注解@DecryptInfoMethodAnnotationpublic 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位就补0String 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;}
}