一、Spring AOP中的一些概念
- 切面(Aspect):切入業務流程的一個獨立模塊。“切面”在ApplicationContext中<aop:aspect>來配置。
- 連接點(Joinpoint) :業務流程在運行過程中需要插入切面的具體位置。
- 通知(Advice) :切面的具體實現方法。可分爲前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)和環繞通知(Around)五種。實現方法具體屬於哪類通知,是在配置文件和註解中指定的。
- 切入點(Pointcut) :定義通知應該切入到哪些連接點上,不同的通知通常需要切入到不同的連接點上。
- 目標對象(Target Object) :被一個或者多個切面所通知的對象。
- AOP代理(AOP Proxy) :在Spring AOP中有兩種代理方式,JDK動態代理和CGLIB代理。默認情況下,TargetObject實現了接口時,則採用JDK動態代理,例如,AServiceImpl;反之,採用CGLIB代理,例如,BServiceImpl。強制使用CGLIB代理需要將 <aop:config>的 proxy-target-class屬性設爲true。
- 織入:將切面應用到目標對象從而創建一個新的代理對象的過程。這個過程可以發生在編譯期、類裝載期及運行期。
通知類型:
- 前置通知(Before advice):在某連接點(JoinPoint)之前執行的通知,但這個通知不能阻止連接點前的執行。ApplicationContext中在<aop:aspect>裏面使用<aop:before>元素進行聲明。例如,TestAspect中的doBefore方法。
- 後置通知(After advice):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>裏面使用<aop:after>元素進行聲明。例如,ServiceAspect中的returnAfter方法,所以Teser中調用UserService.delete拋出異常時,returnAfter方法仍然執行。
- 返回後通知(After return advice):在某連接點正常完成後執行的通知,不包括拋出異常的情況。ApplicationContext中在<aop:aspect>裏面使用<after-returning>元素進行聲明。
- 環繞通知(Around advice):包圍一個連接點的通知,類似Web中Servlet規範中的Filter的doFilter方法。可以在方法的調用前後完成自定義的行爲,也可以選擇不執行。ApplicationContext中在<aop:aspect>裏面使用<aop:around>元素進行聲明。例如,ServiceAspect中的around方法。
- 拋出異常後通知(After throwing advice):在方法拋出異常退出時執行的通知。ApplicationContext中在<aop:aspect>裏面使用<aop:after-throwing>元素進行聲明。例如,ServiceAspect中的returnThrow方法。
注:可以將多個通知應用到一個目標對象上,即可以將多個切面織入到同一目標對象。
二、案例--Spring AOP實現後臺管理日誌
1)自定義註解,攔截Controller
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemControllerLog {
/**
* 描述業務操作 例:Xxx管理-執行Xxx操作
* @return
*/
String description() default "";
}
2)系統日誌切點類
@Aspect
@Component
public class SystemLogAspect {
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect. class);
private static final ThreadLocal<Date> beginTimeThreadLocal =
new NamedThreadLocal<Date>("ThreadLocal beginTime");
private static final ThreadLocal<Log> logThreadLocal =
new NamedThreadLocal<Log>("ThreadLocal log");
private static final ThreadLocal<User> currentUser=new NamedThreadLocal<>("ThreadLocal user");
@Autowired(required=false)
private HttpServletRequest request;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private LogService logService;
/**
* Controller層切點 註解攔截
*/
@Pointcut("@annotation(com.myron.ims.annotation.SystemControllerLog)")
public void controllerAspect(){}
/**
* 方法規則攔截
*/
@Pointcut("execution(* com.myron.ims.controller.*.*(..))")
public void controllerPointerCut(){}
/**
* 前置通知 用於攔截Controller層記錄用戶的操作的開始時間
* @param joinPoint 切點
* @throws InterruptedException
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException{
Date beginTime=new Date();
beginTimeThreadLocal.set(beginTime);
//debug模式下 顯式打印開始時間用於調試
if (logger.isDebugEnabled()){
logger.debug("開始計時: {} URI: {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
.format(beginTime), request.getRequestURI());
}
//讀取session中的用戶
HttpSession session = request.getSession();
User user = (User) session.getAttribute("ims_user");
currentUser.set(user);
}
/**
* 後置通知 用於攔截Controller層記錄用戶的操作
* @param joinPoint 切點
*/
@SuppressWarnings("unchecked")
@After("controllerAspect()")
public void doAfter(JoinPoint joinPoint) {
User user = currentUser.get();
//登入login操作 前置通知時用戶未校驗 所以session中不存在用戶信息
if(user == null){
HttpSession session = request.getSession();
user = (User) session.getAttribute("ims_user");
if(user==null){
return;
}
}
Object [] args = joinPoint.getArgs();
System.out.println(args);
String title="";
String type="info"; //日誌類型(info:入庫,error:錯誤)
String remoteAddr=request.getRemoteAddr();//請求的IP
String requestUri=request.getRequestURI();//請求的Uri
String method=request.getMethod(); //請求的方法類型(post/get)
Map<String,String[]> params=request.getParameterMap(); //請求提交的參數
try {
title=getControllerMethodDescription2(joinPoint);
} catch (Exception e) {
e.printStackTrace();
}
// debu模式下打印JVM信息。
long beginTime = beginTimeThreadLocal.get().getTime();//得到線程綁定的局部變量(開始時間)
long endTime = System.currentTimeMillis(); //2、結束時間
if (logger.isDebugEnabled()){
logger.debug("計時結束:{} URI: {} 耗時: {} 最大內存: {}m 已分配內存: {}m 已分配內存中的剩餘空間: {}m 最大可用內存: {}m",
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(endTime),
request.getRequestURI(),
DateUtils.formatDateTime(endTime - beginTime),
Runtime.getRuntime().maxMemory()/1024/1024,
Runtime.getRuntime().totalMemory()/1024/1024,
Runtime.getRuntime().freeMemory()/1024/1024,
(Runtime.getRuntime().maxMemory()-Runtime.getRuntime().totalMemory()+Runtime.getRuntime().freeMemory())/1024/1024);
}
Log log=new Log();
log.setLogId(UuidUtils.creatUUID());
log.setTitle(title);
log.setType(type);
log.setRemoteAddr(remoteAddr);
log.setRequestUri(requestUri);
log.setMethod(method);
log.setMapToParams(params);
log.setUserId(user.getId());
Date operateDate=beginTimeThreadLocal.get();
log.setOperateDate(operateDate);
log.setTimeout(DateUtils.formatDateTime(endTime - beginTime));
threadPoolTaskExecutor.execute(new SaveLogThread(log, logService));
logThreadLocal.set(log);
}
/**
* 異常通知
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
Log log = logThreadLocal.get();
if(log != null){
log.setType("error");
log.setException(e.toString());
new UpdateLogThread(log, logService).start();
}
}
}
3)Controller層接口代碼:
@Api(value = "/", tags = "系統登入接口")
@Controller
@RequestMapping("/")
public class LoginController {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
/** 登入頁 */
public static final String LOGIN_PAGE = "/index.jsp";
/** 首頁 */
public static final String MAIN_PAGE = "/main.jsp";
/** 用戶session key */
public static final String KEY_USER = "ims_user";
@ApiOperation(value = "登入系統", notes = "登入系統", httpMethod = "POST")
@SystemControllerLog(description="登入系統")
@RequestMapping("/login")
public String login(HttpServletRequest request, ModelMap model,User user, Boolean rememberMe, String verifycode) throws Exception{
//TODO 用戶密碼校驗邏輯省略...
user.setId("0001");
//TODO 驗證碼...
//登入成功
HttpSession session = request.getSession();
session.setAttribute(KEY_USER, user);
logger.info("{} 登入系統成功!", user.getUsername());
model.addAttribute("user", user);
return MAIN_PAGE;
}
/**
* 安全退出登入
* @return
*/
@SystemControllerLog(description="安全退出系統")
@RequestMapping("logout")
public String logout(HttpServletRequest request){
HttpSession session = request.getSession();
User user = (User) session.getAttribute(KEY_USER);
if (user != null) {
//TODO 模擬退出登入,直接清空sessioni
logger.info("{} 退出系統成功!", user.getUsername());
session.removeAttribute(KEY_USER);
}
return LOGIN_PAGE;
}
}
4)請求登錄接口,效果如下:
參考文章:https://blog.csdn.net/myron_007/article/details/54927529#comments
案例源碼:https://github.com/MusicXi/demo-aop-log