目录

  • Spring Aop 自定义注解实现用户登录日志
  • 注解类
  • Aop切面类
  • 配置文件
  • Controller
  • 实体类
  • 总结

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中, 会导致数据库事务失效, 原理一样.

那么就需要在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、用户退出还需要另外配置,正在思考中;