一、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