Spring Aop 自定义注解实现用户登录日志
目前项目是在SSM整合中,想要实现日志记录;
遂用aop自定义注解进行实现
注解类
// 那么首先就需要创建注解类
package com.LoginLog.annotation;
import java.lang.annotation.*;
/**
* Controller aop 拦截
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemControllerLog {
/**
* 日志描述
*
*/
String description() default "";
}
Aop切面类
随后就是创建LogAspect 进行Spring中aop记录日志
package com.LoginLog.aop;
import com.LoginLog.annotation.SystemControllerLog;
import com.LoginLog.entity.LoginLog;
import com.LoginLog.service.LoginLogService;
import com.LoginLog.util.ip.IpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Aspect
@Component
public class LogAspect {
@Autowired
LoginLogService loginLogService;
@Pointcut("@annotation(com.LoginLog.annotation.SystemControllerLog)")
public void ControllerAspect(){
System.out.println("ControllerAspect---加载");
}
/**
* 登录操作后置通知
* 记录登录日志
* 在 Controller 中 login() 执行完之后执行这个方法
* @param
*
* @throws Throwable
*/
// @AfterReturning(value = "execution(* com.LoginLog.Dao.UserDao.login(..))")
@AfterReturning(value = "ControllerAspect()",returning = "returnValue")
public void loginLog(JoinPoint joinPoint,Object returnValue) throws Exception {
//告诉编译器忽略 unchecked 警告信息,如使用List,ArrayList等未进行参数化产生的警告信息。
@SuppressWarnings("unchecked")
// 获取 HttpServletRequest 对象
HttpServletRequest request = getHttpServletRequest();
String userName = request.getParameter("username");
if (userName != null) {
System.out.println("写入日志中......");
}
String context = getServiceMthodDescription(joinPoint);
// 创建日志对象
LoginLog loginLog = new LoginLog();
// 获取 IP 地址
String addr = IpUtils.getIpAddr(request);
loginLog.setUserName(userName);
loginLog.setIpAddr(addr);
loginLog.setStatus(request.getParameter("status"));
loginLog.setMsg(context);
//操作时间
SimpleDateFormat sif = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
loginLog.setLoginTime(sif.format(new Date()));
if (returnValue != null) {
if (returnValue instanceof List) {
List ls = (List) returnValue;
if (ls.size() > 0) {
System.out.println("执行成功");
} else {
System.out.println("执行成功");
}
} else if (returnValue instanceof Boolean) {
Boolean falg = (Boolean) returnValue;
if (falg) {
System.out.println("执行成功");
} else {
System.out.println("执行失败");
}
} else if (returnValue instanceof Integer) {
Integer i = (Integer) returnValue;
if (i > 0) {
System.out.println("执行成功");
} else {
System.out.println("执行失败");
}
} else {
System.out.println("执行成功");
}
// 把登录日志信息存到数据库
loginLogService.insertLog(loginLog);
}
}
/**
*抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
* 注意在这里不能使用ProceedingJoinPoint
* 不然会报错ProceedingJoinPoint is only supported for around advice
* throwing注解为错误信息
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="ControllerAspect()", throwing="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) throws Exception {
HttpServletRequest request = getHttpServletRequest();
String userName = request.getParameter("username");
if (userName != null) {
System.out.println("日志异常......");
}
String context=getServiceMthodDescription(joinPoint);
// 创建日志对象
LoginLog loginLog = new LoginLog();
// 获取 IP 地址
String addr = IpUtils.getIpAddr(request);
loginLog.setUserName(userName);
// loginLog.setLoginTime(new Date());
loginLog.setIpAddr(addr);
loginLog.setStatus(request.getParameter("status"));
loginLog.setMsg(context);
// 把登录日志信息存到数据库
loginLogService.insertLog(loginLog);
}
/**
*获取自定义注解里的日志描述
* @param joinPoint
* @return 返回注解里面的日志描述
* @throws Exception
*/
private String getServiceMthodDescription(JoinPoint joinPoint)
throws Exception {
//类名
String targetName = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = joinPoint.getSignature().getName();
//参数
Object[] arguments = joinPoint.getArgs();
//通过反射获取示例对象
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 == arguments.length) {
//获取注解里的日志信息
description = method.getAnnotation(SystemControllerLog.class).description();
break;
}
}
}
return description;
}
/**
* 获取json格式的参数用于存储到数据库中
* @param joinPoint
* @return
* @throws Exception
*/
private String getServiceMthodParams(JoinPoint joinPoint)
throws Exception {
Object[] arguments = joinPoint.getArgs();
ObjectMapper om=new ObjectMapper();
return om.writeValueAsString(arguments);
}
/**
* 获取当前的request
* 这里如果报空指针异常是因为单独使用spring获取request
* 需要在配置文件里添加监听
* <listener>
* <listener-class>
* org.springframework.web.context.request.RequestContextListener
* </listener-class>
* </listener>
* @return
*/
public HttpServletRequest getHttpServletRequest(){
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)ra;
HttpServletRequest request = sra.getRequest();
return request;
}
}
配置文件
在SSM项目中,因为Spring的Bean扫描和Spring-MVC的Bean扫描是分开的, 两者的Bean位于两个不同的Application, 而且Spring-MVC的Bean扫描要早于Spring的Bean扫描, 所以当Controller Bean生成完成后, 再执行Spring的Bean扫描,Spring会发现要被AOP代理的Controller Bean已经在容器中存在, 配置AOP就无效了.
同样这样的情况也存在于数据库事务中, 如果Service的Bean扫描配置在spring-mvc.xml中, 而数据库事务管理器配置在application.xml中, 会导致数据库事务失效, 原理一样.
上述配置文件参考链接:Spring AOP无法拦截Controller中的方法 - Frank_Hao - 博客园 (cnblogs.com)
那么就需要在SpringMVC中进行Aop的配置:
<!-- 配置登陆日志的 AOP 注解-->
<aop:aspectj-autoproxy/>
<!-- 自动扫描包路径 -->
<!--你需要刚才的切面类的包路径-->
<context:component-scan base-package="com.LoginLog.aop" />
<!--你需要注解方法的包路径-->
<context:component-scan base-package="com.*.*.annotation" />
Controller
实现这些之后,在Controller层添加:
//这一行就是写入日志的了
@SystemControllerLog(description = "登录日志")
@RequestMapping(value = "login")
@ResponseBody
public ModelAndView login(User user, HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView){
User login = userService.login(user);
if (login!=null){
System.out.println("登陆成功!");
request.getSession().setAttribute("login",login);
modelAndView.setViewName("index");
}else {
modelAndView.setViewName("login");
}
return modelAndView;
}
实体类
package com.LoginLog.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
public class LoginLog {
/**
* ID/访问编号
*/
private Long infoId;
/**
* 用户账号名
*/
private String userName;
/**
* 登录地址
*/
private String ipAddr;
/**
* 登录状态 0成功, 1失败
*/
private String status;
/**
* 登录信息
*/
private String msg;
/**
* 登录时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private String loginTime;
public Long getInfoId() {
return infoId;
}
public void setInfoId(Long infoId) {
this.infoId = infoId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getIpAddr() {
return ipAddr;
}
public void setIpAddr(String ipAddr) {
this.ipAddr = ipAddr;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getLoginTime() {
return loginTime;
}
public void setLoginTime(String loginTime) {
this.loginTime = loginTime;
}
@Override
public String toString() {
return "LoginLog{" +
"infoId=" + infoId +
", userName='" + userName + '\'' +
", ipAddr='" + ipAddr + '\'' +
", status='" + status + '\'' +
", msg='" + msg + '\'' +
", loginTime=" + loginTime +
'}';
}
}
总结
1、在SSM项目中,如果想要在Controller层,使用Aop技术须在SpringMVC的配置文件中进行相应配置,而不是在SpringContext的配置文件中;
2、注解开发较为简便,本次只是实现了用户登录;
3、用户退出还需要另外配置,正在思考中;
本次SpringAop结束就到这里,欢迎大家批评指正。