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 |
类型成员(方法 /构造方法/成员变量/枚举值) | CONSTRUCTORFIELD METHOD |
方法中的参数/局部变量 | 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来访问就会正常