Spring AOP的原理及使用實例

一、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。
  • 織入:將切面應用到目標對象從而創建一個新的代理對象的過程。這個過程可以發生在編譯期、類裝載期及運行期。

    通知類型:

  1. 前置通知(Before advice):在某連接點(JoinPoint)之前執行的通知,但這個通知不能阻止連接點前的執行。ApplicationContext中在<aop:aspect>裏面使用<aop:before>元素進行聲明。例如,TestAspect中的doBefore方法。
  2. 後置通知(After advice):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>裏面使用<aop:after>元素進行聲明。例如,ServiceAspect中的returnAfter方法,所以Teser中調用UserService.delete拋出異常時,returnAfter方法仍然執行。
  3. 返回後通知(After return advice):在某連接點正常完成後執行的通知,不包括拋出異常的情況。ApplicationContext中在<aop:aspect>裏面使用<after-returning>元素進行聲明。
  4. 環繞通知(Around advice):包圍一個連接點的通知,類似Web中Servlet規範中的Filter的doFilter方法。可以在方法的調用前後完成自定義的行爲,也可以選擇不執行。ApplicationContext中在<aop:aspect>裏面使用<aop:around>元素進行聲明。例如,ServiceAspect中的around方法。
  5. 拋出異常後通知(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 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章