SSM+自定义注解+AOP实现日志记录
1 需求
工作中,经常会遇到记录日志的动作,以前是使用日志框架来实现,现在可以使用注解来实现,使用起来更方便,随用随加~
今天我们演示在SSM的基础上,对普通的方法加上自定义注解,注解中写上该方法的日志信息,然后将日志信息记录到数据库中.
| 编号 | 用户 | ip | 时间 | 描述 | 
|---|---|---|---|---|
| 1 | admin | 192.168.37.45 | 2023-01-01 17:56:00 | 修改了user信息 | 
| 2 | zs | 192.168.37.35 | 2023-02-01 17:56:00 | 登录 | 
2 技术
Spring+AOP+SpringMVC+Mybatis+注解+反射
3 注解(annotation)
注解,又称为注释.它是给程序看的注释.JDK1.5以后出现的新技术
常见注解: @Override
3.1 创建注解文件
public @interface MyLog {
}
3.2 设置元注解
- 元注解是给注解加的注解
- @Target 作用位置
- @Retention 保留策略
- @Documented 不用
- @Inherited 不用
3.2.1 @Target
定义注解使用的位置
| 使用位置 | EmentType | 
|---|---|
| packaget包上 | PACKAGE | 
| 类/接口/数组/枚举/注解 | TYPE | 
| 类型成员( 方法/构造方法/成员变量/枚举值) | CONSTRUCTOR FIELDMETHOD | 
| 方法中的参数/局部变量 | LOCAL_VARIABLE PARAMETER | 
3.2.2 @Retention
指定注解的保留策略,即注解在什么时期生效.
| RetentionPolicy的值 | 作用 | 
|---|---|
| SOURCE | 在源文件中有效 | 
| CLASS | 在class文件中有效 | 
| RUNTIME | 在运行时有效(即运行时保留),为Runtime可以在反射机制中读取 | 
3.3 设置参数
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    String log(); // 设置了参数,使用注解时必须给该参数赋值
    int type() default 0; // 如果给了default,如果不设置参数,那就是默认值
    // 当参数命名为是其他值时,使用时必须写上参数=值,
    String value();// 当参数名设置成value时,单独使用时可以直接写值,不需要写value属性
}
4 反射
在程序==运行过程==中,获得字节码class文件,从而获得其中的属性和方法包括构造方法.以便于使用这些属性和方法.
4.1 获得字节码文件
    public static void main(String[] args) throws ClassNotFoundException {
        // 获得字节码文件
        // 1.类的静态方法
        Class clazz1 = User.class;
        // 2. Object类的getClass()
        User user = new User( );
        Class clazz2 = user.getClass( );
        // 3.通过类路径获得
        Class clazz3 = Class.forName("com.qf.model.User");
    }
4.2 获得并使用属性
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Class clazz = User.class;
        // 获得属性
        /**
         * getFiled()
         * getFields()
         * 获得公共的属性,即只能获得public修饰的属性
         */
        Field username =  clazz.getField("age");// 根据字段名,获得字段对象
        System.out.println(username );
        Field[] fields = clazz.getFields( ); // 所有字段对象
        for (Field field: fields) {
            System.out.println(field );
        }
        System.out.println("---------" );
        /**
         * getDeclaredField()
         * getDeclaredFields()
         * 获得所声明的所有字段
         */
        Field username1 = clazz.getDeclaredField("username");
        System.out.println(username1 );
        Field[] declaredFields = clazz.getDeclaredFields( );
        for (Field f : declaredFields) {
            System.out.println(f );
        }
        // ==============================
        // 给属性赋值
        User user = new User( );
        user.setPassword("66666");
        Field password = clazz.getDeclaredField("password");
        // 设置访问权限
        password.setAccessible(true);
        // 无法获得私有属性值
        String o = (String)password.get(user);
        System.out.println("利用反射获得字段值: "+o );
        // 需要先设置访问权限
        username1.setAccessible(true);
        // 不能只能直接给私有属性赋值
        username1.set(user,"随意");
        System.out.println(user.getUsername() );
    }
4.3 获得并使用方法
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class clazz = User.class;
        // 获得方法
        /**
         * getMethod(); 只会获得public修饰的方法
         * getDeclaredMethod() 获得所有方法,包括非私有
         */
        Method add1 = clazz.getMethod("add");// 获得的是方法名叫做add的方法,且空参
        Method add2 = clazz.getMethod("add", int.class, int.class);// 获得add方法,且有两个参数,类型是int,int
        Method add3 = clazz.getMethod("add", String.class, String.class);// 获得add方法,且有两个参数,类型是String,String
        // 使用方法
        User user = new User( );
        // add1.setAccessible(true);
        Object r1 = add1.invoke(user);
        System.out.println(r1 );
        Object r2 = add2.invoke(user, 2, 2);
        System.out.println(r2 );
        Object r3 = add3.invoke(user, "4", "4");
        System.out.println(r3 );
    }
4.4 获得并使用构造方法
    public static void main(String[] args) throws Exception {
        Class clazz = User.class;
        /**
         * getConstructor() 获得public修饰构造方法
         * getDeclaredConstructor() 获得所有构造方法,包括私有
         */
        Constructor constructor = clazz.getConstructor( );// 获得空参构造
        // 使用构造方法
        Object o = constructor.newInstance( );// 创建对象
        System.out.println(o );
    }
5 AOP
AOP面向切面(Aspect)编程
面向切面编程的作用:就是将项目中与核心逻辑无关的代码横向抽取成切面类,通过织入作用到目标方法,以使目标方法执行前后达到增强的效果.
原理: AOP底层使用的就是动态代理,给AOP指定哪些类型需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增强的方法.
好处:
- 抽取代码,复用,提高效率
- 减少耦合
- 利于代码扩展

AOP的术语:
目标类(Target): 被代理的类
连接点(JoinPoint): 目标类中准备被切入的方法
切入点(Pointcut): 真正执行的目标方法
切面(Aspect) : 切面中定义中增强的方法
增强(Advice): 也叫通知,就是在目标方法执行前/后的方法
织入(Weaving): 将增强作用到切入点的过程
6 SSM环境
对User表进行操作
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `gender` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `tel` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
INSERT INTO `user` VALUES (1, '张三', 29, '女', '19593741393');
INSERT INTO `user` VALUES (2, '李四', 12, '男', '19593741393');
INSERT INTO `user` VALUES (3, '王五', 32, '女', '19593741393');
INSERT INTO `user` VALUES (5, '赵六', 55, '男', '19593741393');
INSERT INTO `user` VALUES (6, '李青', 62, '男', '19593741393');
INSERT INTO `user` VALUES (7, '福雷格斯', 58, '男', '19693741393');
INSERT INTO `user` VALUES (8, '赵信', 53, '男', '19593741393');
INSERT INTO `user` VALUES (10, '易大师', 70, '男', '19393741393');
INSERT INTO `user` VALUES (11, '蛮族之王', 28, '女', '19293741393');
6.1 pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.qf</groupId>
    <artifactId>ssm_crud_annotation_log</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <!-- servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- SpringMVC依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>
        <!-- Spring-jdbc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>
        <!-- spring面向切面 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.16.RELEASE</version>
        </dependency>
        <!-- spring单元测试 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.16.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <!-- Mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        
        <!--Mybatis-spring适配包-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>
        <!-- 数据库连接池,驱动-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
        <!--数据库连接必须-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!-- json处理-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.1</version>
        </dependency>
    </dependencies>
	<!-- 当mapper在java下时需要设置 -->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>*.xml</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>
6.2 配置文件
6.2.1 applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 注解扫描 -->
    <context:component-scan base-package="com.qf">
        <!--排除controller控制器的包-->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!-- 1加载db.properties
        classpath类路径,即项目的根路径
    -->
    <context:property-placeholder location="classpath:db.properties"/>
    <!-- 2创建数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!-- 注意属性名,要换成driverClassName -->
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.initialSize}"/>
        <property name="minIdle" value="${jdbc.minIdle}"/>
        <property name="maxActive" value="${jdbc.maxActive}"/>
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${jdbc.maxWait}"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/>
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/>
    </bean>
    <!-- 3创建SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 加载mybatis配置文件 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 加载数据源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 4创建Mapper扫描器 -->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- spring扫描mapper所在包,就可以创建接口的代理对象 -->
        <property name="basePackage" value="com.qf.mapper"/>
        <!-- 指定SqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
</beans>
6.2.2 springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!--  配置扫描包-->
    <context:component-scan base-package="com.qf">
        <!-- 只扫描控制器-->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!-- 注解驱动 -->
    <mvc:annotation-driven/>
</beans>
6.2.3 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- 开启下划线转驼峰 -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.qf.model"/>
    </typeAliases>
</configuration>
6.2.4 db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm_log?useSSL=false
jdbc.username=root
jdbc.password=123456
jdbc.initialSize=5
jdbc.minIdle=3
jdbc.maxActive=20
jdbc.maxWait=0
jdbc.timeBetweenEvictionRunsMillis=600000
jdbc.minEvictableIdleTimeMillis=300000
6.2.5 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--服务器启动的时候,加载这个全局配置文件-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!-- 监听服务器启动 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!--注册前端控制器DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--给当前的Servlet配置初始化参数-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--配置DispatcherServlet的映射路径-->
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    <!--编码格式过滤器-->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
6.3 实体类
package com.qf.model;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String gender;
    private String tel;
    // setter & getter
}
6.4 查询全部
6.4.1 UserMapper
package com.qf.mapper;
import com.qf.model.User;
import java.util.List;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
public interface UserMapper {
    List<User> selectAll();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.mapper.UserMapper">
    <select id="selectAll" resultType="User">
        select * from user;
    </select>
</mapper>
6.4.2 UserService
package com.qf.service;
import com.qf.model.User;
import java.util.List;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
public interface UserService {
    List<User> selectAll();
}
package com.qf.service.impl;
import com.qf.mapper.UserMapper;
import com.qf.model.User;
import com.qf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public List<User> selectAll() {
        return userMapper.selectAll();
    }
}
6.4.3 UserController
package com.qf.controller;
import com.qf.model.User;
import com.qf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@RestController // 该类中所有方法返回的都是JSON数据
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/list")
    public List<User> selectAll() {
        return userService.selectAll();
    }
}
6.5 测试
启动服务器,测试
7 自定义注解实现日志记录[重点]
首先设计日志表,将来存储日志信息
CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `log_time` datetime DEFAULT NULL,
  `log` varchar(255) DEFAULT NULL,
  `ip` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ps: 数据库字段是下划线,mybatis配置文件中要开驼峰转换才可以自动完成orm.
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
// 日志类
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Log {
    private int id;
    private String name;
    private Date logTime;
    private String log;
    private String ip;
}
7.1 日志注解文件
package com.qf.annotation;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    String value();
}
7.2 切面类
package com.qf.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import com.qf.annotation.MyLog;
import com.qf.model.Log;
import com.qf.model.User;
import com.qf.service.MyLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Component
@Aspect
public class MyLogAspect {
    @Autowired
    private MyLogService logService;
    /**
     * 后置增强
     */
    @After("@annotation(com.taotie.aop_log_aspect.Log)")
    public void saveLog(JoinPoint joinPoint) {
        // 1 获得时间
        Date date = new Date( );
        // 2 获得ip
        // 3 获得当前操作的用户名
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes( )).getRequest( );
        // 当时登录成功后,存入session数据,session中的key为user
        User user = (User) WebUtils.getSessionAttribute(request, "user");
        String username = null;
        if (user != null) {
            username = user.getUsername();
        }
        String ip = request.getRemoteAddr( );
        // 4 获得日志描述
        // 获得目标类
        Object target = joinPoint.getTarget( );
        Method[] declaredMethods = target.getClass( ).getDeclaredMethods( );
        String logDesc = null;
        String targetMethodName = joinPoint.getSignature( ).getName( );
        for (Method method : declaredMethods) {
            if (method.getName().equals(targetMethodName)) {
                Log log = method.getAnnotation(Log.class);
                if (log != null) {
                    logDesc = log.value( );
                }
            }
        }
        com.taotie.model.Log log = new com.taotie.model.Log( );
        log.setIp(ip);
        log.setName(username);
        log.setLogTime(date);
        log.setLog(logDesc);
        /**
         * 插入数据库
         */
        logService.saveLog(log);
    }
}
7.3 使用注解
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/login")
    public User login(String name, HttpSession session) {
        User user = userService.selectUserByUsername(name);
        if (user != null) {
            session.setAttribute("user",user);
        }
        return user;
    }
    @GetMapping("/list")
    @MyLog("查询全部") // 使用日志注解
    public List<User> selectAll() {
        return userService.selectAll();
    }
}
7.4 aop扫描
    <!-- 开启aop注解
日志注解加在业务层,aop注解驱动设置在applicationContext.xml 
日志注解加在控制层,aop注解驱动设置在springmvc.xml
-->
    <aop:aspectj-autoproxy/>
7.5 测试
浏览器访问测试

ip是因为使用localhost访问,换成127.0.0.1来访问就会正常





![[VUE学习]权限管理系统前端vue实现8-右上角用户头像显示实现](https://img-blog.csdnimg.cn/7039e9fba51847f3ad3cdefe5f78e238.png)













