说明
- Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足。
- 还有就是为了清晰的逻辑,让业务逻辑关注业务本身,不用去关心其它的事情,比如事务。
专业术语简单解释
- 通知(有的地方叫增强)(Advice):需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用
- 连接点(Join point):就是spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点
- 切点(Poincut):其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点
- 切面(Aspect):其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行
- 引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去
- 目标(target):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。
- 织入(Weaving):把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:
完整代码地址在结尾!!
第一步,在pom.xml加入依赖,如下
<!-- SpringBoot-AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步,编写application.yml配置文件,如下
server:
port: 8081
spring:
application:
name: aop-demo-server
http:
check:
key:
aes:
request: w@sd8dlm # 解密请求体默认key
response: ems&koq3 # 加密响应体默认key
request:
timeout: 10 # 请求时间超时时间,单位秒
第三步,创建KeyConfig配置文件,如下
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class KeyConfig {
/**
* 解密请求体默认key
*/
@Value("${http.check.key.aes.request}")
private String keyAesRequest;
/**
* 加密响应体默认key
*/
@Value("${http.check.key.aes.response}")
private String KeyAesResponse;
/**
* 请求时间超时时间,单位秒
*/
@Value("${http.check.request.timeout}")
private String timeout;
}
第四步,创建工具类,JsonUtils,AESUtil,如下
JsonUtils
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* JsonUtils
*
* @author luoyu
* @date 2018/10/08 19:13
* @description Json工具类,依赖 jackson
*/
@Slf4j
public class JsonUtils {
private static ObjectMapper objectMapper = null;
static {
objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
/**
* 对象转换成json
* @param obj
* @param <T>
* @return
*/
public static <T>String objectToJson(T obj){
if(obj == null){
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.error("Parse Object to Json error",e);
return null;
}
}
/**
* 将json转换成对象Class
* @param src
* @param clazz
* @param <T>
* @return
*/
public static <T>T jsonToObject(String src,Class<T> clazz){
if(StringUtils.isEmpty(src) || clazz == null){
return null;
}
try {
return clazz.equals(String.class) ? (T) src : objectMapper.readValue(src,clazz);
} catch (Exception e) {
log.warn("Parse Json to Object error",e);
return null;
}
}
/**
* 字符串转换为 Map<String, Object>
*
* @param src
* @return
* @throws Exception
*/
public static <T> Map<String, Object> jsonToMap(String src) {
if(StringUtils.isEmpty(src)){
return null;
}
try {
return objectMapper.readValue(src, Map.class);
} catch (Exception e) {
log.warn("Parse Json to Map error",e);
return null;
}
}
public static <T> List<T> jsonToList(String jsonArrayStr, Class<T> clazz) {
try{
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, clazz);
return (List<T>) objectMapper.readValue(jsonArrayStr, javaType);
}catch (Exception e) {
log.warn("Parse Json to Map error",e);
return null;
}
}
}
AESUtil
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* @version 1.0
* @author luoyu
* @date 2018-05-09
* @description AES工具类
*/
public class AESUtil {
private final static String KEY_ALGORITHM = "AES";
//默认的加密算法
private final static String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* @Author: jinhaoxun
* @Description: AES 加密操作
* @param content 待加密内容
* @param key 加密密钥
* @Date: 2020/4/2 上午12:46
* @Return: javax.crypto.spec.SecretKeySpec 返回Base64转码后的加密数据
* @Throws:
*/
public static String encrypt(String content, String key) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// 初始化为加密模式的密码器
byte[] result = cipher.doFinal(byteContent);// 加密
return new String(Base64Utils.encode(result));//通过Base64转码返回
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* @Author: jinhaoxun
* @Description: AES 解密操作
* @param content
* @param key
* @Date: 2020/4/2 上午12:46
* @Return: javax.crypto.spec.SecretKeySpec
* @Throws:
*/
public static String decrypt(String content, String key) {
try {
//实例化
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
//使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
//执行操作
byte[] result = cipher.doFinal(Base64Utils.decode(content.getBytes()));
return new String(result, "utf-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* @Author: jinhaoxun
* @Description: 生成加密秘钥
* @param key
* @Date: 2020/4/2 上午12:46
* @Return: javax.crypto.spec.SecretKeySpec
* @Throws:
*/
private static SecretKeySpec getSecretKey(final String key) {
//返回生成指定算法密钥生成器的 KeyGenerator 对象
KeyGenerator kg = null;
try {
kg = KeyGenerator.getInstance(KEY_ALGORITHM);
// 解决操作系统内部状态不一致问题(部分liunx不指定类型,无法解密)
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
kg.init(128, secureRandom);
// kg.init(128, new SecureRandom(key.getBytes()));
//生成一个密钥
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
第五步,创建实体类,HttpRequest,HttpResponse,TestRequest,TestResponse,如下
HttpRequest
import lombok.Data;
import javax.validation.Valid;
/**
* @Description: Http的请求的大对象
* @author jinhaoxun
* @date 2019年12月29日 下午8:16:52
*/
@Data
public class HttpRequest<T>{
/**
* 客户端的版本
*/
private String version;
/**
* 客户端的渠道
*/
private String channel;
/**
* 客户端发起请求的时间
*/
private long time;
/**
* 请求的数据对象
*/
@Valid
private T data;
/**
* 请求的密文,如果该接口需要加密上送,
* 则将sdt的密文反序化到data,
* sdt和action至少有一个为空
*/
private String sdt;
}
HttpResponse
import lombok.Data;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
/**
* @Description: Http的响应大对象
* @author jinhaoxun
* @date 2019年12月29日 下午8:15:39
*/
@Data
public class HttpResponse<T>{
/**
* 响应码
*/
private Integer code;
/**
* 响应时间
*/
private long time;
/**
* 响应的信息(一般为错误信息)
*/
private String msg;
/**
* 响应数据(一般为ActionResponse的子类)
*/
private T data;
/**
* 响应的密文,如果该接口需要加密返回,
* 则将data的密文绑定到该字段上,
* srs和data至少有一个为空
*/
private String srs;
/**
* 私有化默认构造器
*/
private HttpResponse() {
}
private HttpResponse(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.time = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();;
}
/**
* @Description: 构建一个响应
* @author jinhaoxun
* @date 2019年1月2日 下午2:09:24
* @param code
* @param msg
* @param data
* @return
*/
public static <T> HttpResponse<T> build(Integer code, String msg, T data){
return new HttpResponse<T>(code, msg, data);
}
}
TestRequest
import lombok.Data;
/**
* @Description:
* @Author: jinhaoxun
* @Date: 2020/7/10 10:36 上午
* @Version: 1.0.0
*/
@Data
public class TestRequest{
private int id;
private String name;
private String sex;
}
TestResponse
import lombok.Data;
/**
* @Description:
* @Author: jinhaoxun
* @Date: 2020/7/10 10:36 上午
* @Version: 1.0.0
*/
@Data
public class TestResponse{
private int id;
private String name;
private String sex;
}
第六步,创建aop类,HttpCheck,HttpCheckAspact,如下
HttpCheck
import java.lang.annotation.*;
/**
*
* @Description: http请求的参数校验
* @author luoyu
* @date 2019年1月9日 下午6:53:41
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpCheck {
/**
* 是否需要解密(默认需要解密)
*/
boolean isDecrypt() default true;
/**
* 解密的key,只有当isDecrypt=true
* 才会检测该字段,并且传入为空时,
* 用系统预先设置的key进行解密。
*/
String decryptKey() default "";
/**
* 解密,系统统一加密反序列化的类
*/
Class<?> dataType();
/**
* 是否需要加密返回(默认加密返回)
*/
boolean isEncrypt() default true;
/**
* 加密的key,只有当isEncrypt=true
* 才会检测该字段,并且传入为空时,
* 用系统预先设置的key进行加密返回
*/
String encryptKey() default "";
/**
* 是否需要检测超时时间(默认需要)
*/
boolean isTimeout() default true;
/**
* 超时时间,只有当isTimeout=true
* 才会检测该字段,并且传入为空时,
* 用系统预先设置的timeout进行加密返回
*/
String timeout() default "";
}
HttpCheckAspact
import com.luoyu.aop.config.KeyConfig;
import com.luoyu.aop.entity.http.HttpRequest;
import com.luoyu.aop.entity.http.HttpResponse;
import com.luoyu.aop.entity.response.TestResponse;
import com.luoyu.aop.util.AESUtil;
import com.luoyu.aop.util.JsonUtils;
import com.oracle.tools.packager.Log;
import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
/**
* @Auther: luoyu
* @Date: 2019/1/22 16:28
* @Description:
*/
@Aspect
@Component
public class HttpCheckAspact {
@Autowired
private KeyConfig keyConfig;
@Pointcut("@annotation(com.luoyu.aop.aop.HttpCheck)")
public void pointcut() {
}
/**
* 前置处理
* @param joinPoint
* @throws Exception
*/
@SuppressWarnings("unchecked")
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) throws Exception {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
HttpCheck annotation = method.getAnnotation(HttpCheck.class);
// 获取HttpRequest对象
Object[] args = joinPoint.getArgs();
HttpRequest httpRequest = null;
if(args.length > 0){
for(Object arg: args){
if(arg instanceof HttpRequest){
httpRequest = (HttpRequest)arg;
}
}
}else {
throw new Exception("请求参数错误!");
}
// 是否需要检测超时时间
if (annotation.isTimeout()){
// 获取超时时间
String timeout = StringUtils.isEmpty(annotation.timeout()) ? keyConfig.getTimeout(): annotation.timeout();
if(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() < httpRequest.getTime()){
throw new Exception("请求时间错误!");
}
if(LocalDateTime.now().minusSeconds(Integer.parseInt(timeout)).toInstant(ZoneOffset.of("+8")).toEpochMilli() > httpRequest.getTime()){
throw new Exception("请求已超时!");
}
Log.info("检测超时时间成功!");
}
// 是否需要进行解密
if(annotation.isDecrypt()){
// 获取需要解密的key
String dectyptKey = StringUtils.isEmpty(annotation.decryptKey()) ? keyConfig.getKeyAesRequest(): annotation.decryptKey();
String sdt = httpRequest.getSdt();
if(StringUtils.isEmpty(sdt)){
throw new Exception("sdt不能为空!");
}
String context = AESUtil.decrypt(sdt, dectyptKey);
if(StringUtils.isEmpty(context)){
throw new Exception("sdt解密出错!");
}
Log.info("解密成功!");
// 设置解密后的data
httpRequest.setData(JsonUtils.jsonToObject(context, annotation.dataType()));
}
}
@AfterReturning(value = "pointcut()", returning = "response")
public void doAfterReturning(JoinPoint joinPoint, Object response) throws Exception {
HttpResponse httpResponse = (HttpResponse) response;
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
HttpCheck annotation = method.getAnnotation(HttpCheck.class);
if(annotation.isEncrypt()){
TestResponse body = (TestResponse) httpResponse.getData();
// 进行响应加密
if (body != null) {
String encrypyKey = StringUtils.isEmpty(annotation.encryptKey())? keyConfig.getKeyAesResponse() : annotation.encryptKey();
// 设置加密后的srs
httpResponse.setSrs(AESUtil.encrypt(JsonUtils.objectToJson(body), encrypyKey));
Log.info("加密成功!");
httpResponse.setData(null);
}
}
}
}
第七步,创建服务类,TestService,TestServiceImpl,如下
TestService
import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;
/**
* @Description:
* @Author: luoyu
* @Date: 2020/7/10 10:31 上午
* @Version: 1.0.0
*/
public interface TestService {
TestResponse get(TestRequest testRequest) throws Exception;
}
TestServiceImpl
import com.luoyu.aop.service.TestService;
import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;
import org.springframework.stereotype.Service;
/**
* @Description:
* @Author: luoyu
* @Date: 2020/7/10 10:32 上午
* @Version: 1.0.0
*/
@Service
public class TestServiceImpl implements TestService {
@Override
public TestResponse get(TestRequest testRequest) throws Exception {
TestResponse testResponse = new TestResponse();
testResponse.setId(testRequest.getId());
testResponse.setName(testRequest.getName());
testResponse.setSex(testRequest.getSex());
return testResponse;
}
}
第八步,创建TestController类,如下
import com.luoyu.aop.aop.HttpCheck;
import com.luoyu.aop.service.TestService;
import com.luoyu.aop.entity.http.HttpRequest;
import com.luoyu.aop.entity.http.HttpResponse;
import com.luoyu.aop.entity.request.TestRequest;
import com.luoyu.aop.entity.response.TestResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Description:
* @Author: luoyu
* @Date: 2020/7/10 10:31 上午
* @Version: 1.0.0
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Resource
TestService testService;
/**
* @author luoyu
* @description 测试接口1
*/
@PostMapping(value = "/test1", produces = "application/json; charset=UTF-8")
@HttpCheck(dataType = TestRequest.class)
public HttpResponse<TestResponse> Test1(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
TestResponse testResponse = testService.get(httpRequest.getData());
return HttpResponse.build(200, "成功!", testResponse);
}
/**
* @author luoyu
* @description 测试接口2
*/
@PostMapping(value = "/test2", produces = "application/json; charset=UTF-8")
@HttpCheck(dataType = TestRequest.class, isTimeout = false)
public HttpResponse<TestResponse> Test2(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
TestResponse testResponse = testService.get(httpRequest.getData());
return HttpResponse.build(200, "成功!", testResponse);
}
/**
* @author luoyu
* @description 测试接口3
*/
@PostMapping(value = "/test3", produces = "application/json; charset=UTF-8")
@HttpCheck(dataType = TestRequest.class, isDecrypt = false, isTimeout = false)
public HttpResponse<TestResponse> Test3(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
TestResponse testResponse = testService.get(httpRequest.getData());
return HttpResponse.build(200, "成功!", testResponse);
}
/**
* @author luoyu
* @description 测试接口4
*/
@PostMapping(value = "/test4", produces = "application/json; charset=UTF-8")
@HttpCheck(dataType = TestRequest.class, isEncrypt = false, isTimeout = false)
public HttpResponse<TestResponse> Test4(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
TestResponse testResponse = testService.get(httpRequest.getData());
return HttpResponse.build(200, "成功!", testResponse);
}
/**
* @author luoyu
* @description 测试接口5
*/
@PostMapping(value = "/test5", produces = "application/json; charset=UTF-8")
@HttpCheck(dataType = TestRequest.class, isDecrypt = false, isEncrypt = false, isTimeout = false)
public HttpResponse<TestResponse> Test5(@RequestBody HttpRequest<TestRequest> httpRequest) throws Exception {
TestResponse testResponse = testService.get(httpRequest.getData());
return HttpResponse.build(200, "成功!", testResponse);
}
}
第九步,创建单元测试类,AopApplicationTests,对简单实体参数进行加解密,如下
import com.luoyu.aop.config.KeyConfig;
import com.luoyu.aop.entity.response.TestResponse;
import com.luoyu.aop.util.AESUtil;
import com.luoyu.aop.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
// 获取启动类,加载配置,确定装载 Spring 程序的装载方法,它回去寻找 主配置启动类(被 @SpringBootApplication 注解的)
@SpringBootTest
class AopApplicationTests {
@Autowired
private KeyConfig keyConfig;
@Test
void AESEncryptRequestTest() throws Exception {
TestResponse testResponse = new TestResponse();
testResponse.setId(1);
testResponse.setName("test");
testResponse.setSex("男");
String encrypt = AESUtil.encrypt(JsonUtils.objectToJson(testResponse), keyConfig.getKeyAesRequest());
log.info(encrypt);
}
@Test
void AESDecryptRequestTest() throws Exception {
String str = "AdChWsCSrehSLAJVUalBseXKZ7BVQ0RS5hd5EryE+hE2GZ+upLPM1hR2kgCwseeF";
String decrypt = AESUtil.decrypt(str, keyConfig.getKeyAesRequest());
log.info(decrypt);
}
@Test
void AESEncryptResponseTest() throws Exception {
TestResponse testResponse = new TestResponse();
testResponse.setId(1);
testResponse.setName("test");
testResponse.setSex("男");
String encrypt = AESUtil.encrypt(JsonUtils.objectToJson(testResponse), keyConfig.getKeyAesResponse());
log.info(encrypt);
}
@Test
void AESDecryptResponseTest() throws Exception {
String str = "ReXg0r2PHewqdtD/ucSUU05UtLcbNSaPiTWzQj6EHGqtDrokVclzeTMlow5OPthC";
String decrypt = AESUtil.decrypt(str, keyConfig.getKeyAesResponse());
log.info(decrypt);
}
@BeforeEach
void testBefore(){
log.info("测试开始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
@AfterEach
void testAfter(){
log.info("测试结束!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
}
第十步,验证流程如下
- 使用AopApplicationTests测试类,对简单实体参数进行加解密
- 然后用加密后的参数,使用postman工具调用api接口
- 调用TestController不同的api接口,通过出入参数的不同,验证是否实现功能
注:此工程包含多个module,本文所用代码均在aop-demo模块下