SpringBoot AOP導(dǎo)致service注入后是null的問題
SpringBoot AOP導(dǎo)致service注入后是null
1.由于業(yè)務(wù)需求需要
記錄用戶操作日志,無疑需要使用到SpringAOP。
2.先引入SpringBoot的AOP maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>3.實現(xiàn)這個日志操作可以有很多種方法
比如寫攔截器,或者基于注解形式,在或者在原來寫好的代碼中添加也行,但是最后的一種無疑會污染原來的代碼,同時會對原來的邏輯有一定污染,而現(xiàn)在的業(yè)務(wù)場景,所有的邏輯代碼都已經(jīng)編寫完成,自測完成,而且查詢接口的數(shù)量明顯大于(增刪改),而且用戶的操作日志我們最關(guān)心的無疑是對數(shù)據(jù)的操作。所以選擇基于注解的形式實現(xiàn)。
4.注解編寫
上代碼
@Retention(RetentionPolicy.RUNTIME)//元注解,定義注解被保留策略,一般有三種策略
//1、RetentionPolicy.SOURCE 注解只保留在源文件中,在編譯成class文件的時候被遺棄
//2、RetentionPolicy.CLASS 注解被保留在class中,但是在jvm加載的時候北歐拋棄,這個是默認(rèn)的聲明周期
//3、RetentionPolicy.RUNTIME 注解在jvm加載的時候仍被保留
@Target({ElementType.METHOD}) //定義了注解聲明在哪些元素之前
@Documented
public @interface SystemOperaLog {
//定義成員
String descrption() default "" ;//描述
String actionType() default "添加" ;//操作的類型,1、添加 2、修改 3、刪除
}5.下面需要對SpringBoot AOP 中一些注解
了解一下
- @Aspect:描述一個切面類,定義切面類的時候需要打上這個注解
- @Configuration:spring-boot配置類
- @Pointcut:聲明一個切入點,切入點決定了連接點關(guān)注的內(nèi)容,使得我們可以控制通知什么時候執(zhí)行。Spring AOP只支持Spring bean的方法執(zhí)行連接點。所以你可以把切入點看做是Spring bean上方法執(zhí)行的匹配。一個切入點聲明有兩個部分:一個包含名字和任意參數(shù)的簽名,還有一個切入點表達(dá)式,該表達(dá)式?jīng)Q定了我們關(guān)注那個方法的執(zhí)行。
注:作為切入點簽名的方法必須返回void 類型
Spring AOP支持在切入點表達(dá)式中使用如下的切入點指示符:
- execution - 匹配方法執(zhí)行的連接點,這是你將會用到的Spring的最主要的切入點指示符。
- within - 限定匹配特定類型的連接點(在使用Spring AOP的時候,在匹配的類型中定義的方法的執(zhí)行)。
- this - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中bean reference(Spring AOP 代理)是指定類型的實例。
- target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中目標(biāo)對象(被代理的應(yīng)用對象)是指定類型的實例。
- args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中參數(shù)是指定類型的實例。
- @target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中正執(zhí)行對象的類持有指定類型的注解。
- @args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中實際傳入?yún)?shù)的運行時類型持有指定類型的注解。
- @within - 限定匹配特定的連接點,其中連接點所在類型已指定注解(在使用Spring AOP的時候,所執(zhí)行的方法所在類型已指定注解)。
- @annotation - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中連接點的主題持有指定的注解。
其中execution使用最頻繁,即某方法執(zhí)行時進行切入。定義切入點中有一個重要的知識,即切入點表達(dá)式,我們一會在解釋怎么寫切入點表達(dá)式。
切入點意思就是在什么時候切入什么方法,定義一個切入點就相當(dāng)于定義了一個“變量”,具體什么時間使用這個變量就需要一個通知。
即將切面與目標(biāo)對象連接起來。
如例子中所示,通知均可以通過注解進行定義,注解中的參數(shù)為切入點。
spring aop支持的通知:
- @Before:前置通知:在某連接點之前執(zhí)行的通知,但這個通知不能阻止連接點之前的執(zhí)行流程(除非它拋出一個異常)。
- @AfterReturning :后置通知:在某連接點正常完成后執(zhí)行的通知,通常在一個匹配的方法返回的時候執(zhí)行。
6.了解完以上步驟
需要做的是記錄用戶正常請求,以及異常請求需要記錄的信息,對應(yīng)一下實體操作
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="SysOperaLog對象", description="")
public class SysOperaLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "log_id", type = IdType.AUTO)
private Integer logId;
@ApiModelProperty(value = "操作類型")
private String type;
@ApiModelProperty(value = "訪問資源路徑")
private String url;
@ApiModelProperty(value = "操作人員")
private String operaUser;
@ApiModelProperty(value = "方法名稱")
private String methodName;
@ApiModelProperty(value = "訪問攜帶參數(shù)")
private String params;
@ApiModelProperty(value = "遠(yuǎn)程ip地址")
private String ipAddress;
@ApiModelProperty(value = "訪問時間")
@TableField("operaTime")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operaTime;
@ApiModelProperty(value = "訪問總時長單位毫秒")
@TableField("timeLong")
private Long timeLong;
@ApiModelProperty(value = "描述")
private String des;
@ApiModelProperty(value = "訪問狀態(tài) 0 訪問成功 -1 訪問失敗")
private Integer visitState;
@ApiModelProperty(value = "異常信息")
private String exceptionDetail;
}7.用戶正常請求
在用戶正常請求頭部加上自定義的注解
@PostMapping("/save")
@SystemOperaLog(descrption = "新增車位信息")
public Result saveParkLot(@ApiParam(name = "parkLotsDTO", value = "車位信息") @RequestBody ParkLotsDTO parkLotsDTO, BindingResult bindingResult) {
// 現(xiàn)在表示執(zhí)行的驗證出現(xiàn)錯誤
if (bindingResult.hasErrors()) {
// 獲取全部錯誤信息
List<ObjectError> allErrors = bindingResult.getAllErrors();
String errorMsg = "";
if (!CollectionUtils.isEmpty(allErrors)) {
errorMsg = allErrors.get(0).getDefaultMessage();
}
return ResultSupport.fail(errorMsg);
} else {
parkLotsService.save(parkLotsDTO);
return ResultSupport.saveSuccess();
}
}8.在AOP中攔截進入該方法的前置操作以及異常操作
@Aspect
@Configuration
@Slf4j
public class SystemOperaLogAop {
@Autowired
private SysOperaLogMapper sysOperaLogMapper;
/***
* 定義controller切入點攔截規(guī)則,攔截SystemControllerLog注解的方法
*/
@Pointcut("@annotation(cn.hayll.parking.local.common.annotation.SystemOperaLog)")
public void controllerAspect(){}
/***
* 攔截控制層的操作日志
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("controllerAspect()")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//1、開始時間
long beginTime = System.currentTimeMillis();
//利用RequestContextHolder獲取requst對象
ServletRequestAttributes requestAttr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
String uri = requestAttr.getRequest().getServletPath();
//訪問目標(biāo)方法的參數(shù) 可動態(tài)改變參數(shù)值
Object[] args = joinPoint.getArgs();
//方法名獲取
String methodName = joinPoint.getSignature().getName();
//可能在反向代理請求進來時,獲取的IP存在不正確行 這里直接摘抄一段來自網(wǎng)上獲取ip的代碼
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SysUserDetails sysUserDetails = (SysUserDetails) authentication.getPrincipal();
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暫不支持非方法注解");
}
//調(diào)用實際方法
Object object = joinPoint.proceed();
//獲取執(zhí)行的方法
MethodSignature methodSign = (MethodSignature) signature;
Method method = methodSign.getMethod();
//判斷是否包含了 無需記錄日志的方法
long endTime = System.currentTimeMillis();
//模擬異常
//System.out.println(1/0);
SysOperaLog systemLogDTO = new SysOperaLog();
systemLogDTO.setType(getAnnontationMethodDescription(joinPoint,1));
systemLogDTO.setUrl(uri);
systemLogDTO.setOperaUser(sysUserDetails.getUsername());
systemLogDTO.setMethodName(methodName);
systemLogDTO.setIpAddress(getIpAddr(requestAttr.getRequest()));
List list = CollectionUtils.arrayToList(args);
List arrayList = new ArrayList(list);
//移除操作需要將數(shù)組轉(zhuǎn)換的集合類型在此轉(zhuǎn)換為集合類型
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()){
if(iterator.next().toString().contains("BeanPropertyBindingResult")){
iterator.remove();
break;
}
}
systemLogDTO.setParams(arrayList.toString());
systemLogDTO.setOperaTime(LocalDateTime.now());
systemLogDTO.setDes(getAnnontationMethodDescription(joinPoint,0));
systemLogDTO.setTimeLong(endTime - beginTime);
sysOperaLogMapper.insert(systemLogDTO);
return object;
}
//異常處理
@AfterThrowing(pointcut = "controllerAspect()",throwing="e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) throws Throwable{
//1、開始時間
long beginTime = System.currentTimeMillis();
//利用RequestContextHolder獲取requst對象
ServletRequestAttributes requestAttr = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
String uri = requestAttr.getRequest().getServletPath();
//訪問目標(biāo)方法的參數(shù) 可動態(tài)改變參數(shù)值
Object[] args = joinPoint.getArgs();
//方法名獲取
String methodName = joinPoint.getSignature().getName();
//可能在反向代理請求進來時,獲取的IP存在不正確行 這里直接摘抄一段來自網(wǎng)上獲取ip的代碼
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SysUserDetails sysUserDetails = (SysUserDetails) authentication.getPrincipal();
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暫不支持非方法注解");
}
//獲取執(zhí)行的方法
MethodSignature methodSign = (MethodSignature) signature;
Method method = methodSign.getMethod();
//判斷是否包含了 無需記錄日志的方法
long endTime = System.currentTimeMillis();
//模擬異常
SysOperaLog systemLogDTO = new SysOperaLog();
systemLogDTO.setUrl(uri);
systemLogDTO.setType(getAnnontationMethodDescription(joinPoint,1));
systemLogDTO.setOperaUser(sysUserDetails.getUsername());
systemLogDTO.setMethodName(methodName);
systemLogDTO.setIpAddress(getIpAddr(requestAttr.getRequest()));
List list = CollectionUtils.arrayToList(args);
List arrayList = new ArrayList(list);
//移除操作需要將數(shù)組轉(zhuǎn)換的集合類型在此轉(zhuǎn)換為集合類型
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()){
if(iterator.next().toString().contains("BeanPropertyBindingResult")){
iterator.remove();
break;
}
}
systemLogDTO.setParams(arrayList.toString());
systemLogDTO.setOperaTime(LocalDateTime.now());
systemLogDTO.setTimeLong(endTime - beginTime);
systemLogDTO.setDes(getAnnontationMethodDescription(joinPoint,0));
systemLogDTO.setVisitState(-1);
systemLogDTO.setExceptionDetail(e.getMessage());
sysOperaLogMapper.insert(systemLogDTO);
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根據(jù)網(wǎng)卡取本機配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("獲取ip異常:{}" ,e.getMessage());
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 對于通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
/***
* 獲取controller的操作信息
* @param point
* @return
*/
public String getAnnontationMethodDescription(JoinPoint point,Integer type) throws Exception{
//獲取連接點目標(biāo)類名
String targetName = point.getTarget().getClass().getName() ;
//獲取連接點簽名的方法名
String methodName = point.getSignature().getName() ;
//獲取連接點參數(shù)
Object[] args = point.getArgs() ;
//根據(jù)連接點類的名字獲取指定類
Class targetClass = Class.forName(targetName);
//獲取類里面的方法
Method[] methods = targetClass.getMethods() ;
String description="" ;
for (Method method : methods) {
if (method.getName().equals(methodName)){
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == args.length){
if(type==0){
description = method.getAnnotation(SystemOperaLog.class).descrption();
break;
}else{
description = method.getAnnotation(SystemOperaLog.class).actionType();
break;
}
}
}
}
return description ;
}9.說一下踩的一個坑
現(xiàn)在日志已經(jīng)可以正常工作了,但是業(yè)務(wù)代碼卻失效了,service注入的時候是空的(部分代碼),
一頓百度以后發(fā)現(xiàn),原來AOP只能對public 和provide 生效,如果你的方法限制是private,那么service注入就為空,在springboot 中默認(rèn)使用的是cglib來代理操作對象,首先,私有方法是不會出現(xiàn)在代理類中,這也就是為什么代理對象無法對private操作的根本原因
- jdk是代理接口,私有方法必然不會存在在接口里,所以就不會被攔截到;
- cglib是子類,private的方法照樣不會出現(xiàn)在子類里,也不能被攔截。
10.解決的根本辦法
不是強制使用cglib來代理,而是要將你的controller中的方法不設(shè)置私有屬性,以上僅僅代表個人觀點喲。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java利用Redis實現(xiàn)高并發(fā)計數(shù)器的示例代碼
這篇文章主要介紹了Java利用Redis實現(xiàn)高并發(fā)計數(shù)器的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
Springboot整合quartz實現(xiàn)多個定時任務(wù)實例
這篇文章主要介紹了Springboot整合quartz實現(xiàn)多個定時任務(wù)代碼實例,Quartz?是一款功能強大的開源任務(wù)調(diào)度框架,幾乎可以集成到任何?Java?應(yīng)用程序中,Quartz?可用于創(chuàng)建簡單或復(fù)雜的任務(wù)調(diào)度,用以執(zhí)行數(shù)以萬計的任務(wù),需要的朋友可以參考下2023-08-08
Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程
這篇文章主要介紹了Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子 ,實現(xiàn)了小程序的自定義登陸,將自定義登陸態(tài)token返回給小程序作為登陸憑證。需要的朋友可以參考下2018-11-11

