說明
- 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模塊下