一、需求描述与分析
客户侧提出需求很简单:要对几个关键的业务功能进行操作日志记录,即什么人在什么时间操作了哪个功能,操作前的数据报文是什么、操作后的数据报文是什么,必要的时候可以一键回退。
日志在业务系统中是必不可少的一个功能,常见的有系统日志、操作日志等:
1、系统日志
这里的系统日志是指的是程序执行过程中的关键步骤,根据实际场景输出的debug、info、warn、error等不同级别的程序执行记录信息,这些一般是给程序员或运维看的,一般在出现异常问题的时候,可以通过系统日志中记录的关键参数信息和异常提示,快速排除故障。
2、操作日志
操作日志,是用户实际业务操作行为的记录,这些信息一般存储在数据库里,如什么时间哪个用户点了某个菜单、修改了哪个配置等这类业务操作行为,这些日志信息是给普通用户或系统管理员看到。
3、通过对需求的分析,客户想要是一个业务操作日志管理的功能:
- 记录用户的业务操作行为,记录的字段有:操作人、操作时间、操作功能、日志类型、操作内容描述、操作内容报文、操作前内容报文。
- 提供一个可视化的页面,可以查询用户的业务操作行为,对重要操作回溯。
- 提供一定的管理功能,必要的时候可以对用户的误操作回滚。
4、设计思路
这里使用aop实现:
-
定义业务操作日志注解,注解内可以定义一些属性,如操作功能名称、功能的描述等;
-
把业务操作日志注解标记在需要进行业务操作记录的方法上(在实际业务中,一些简单的业务查询行为通常没有必要记录);
-
定义切入点,编写切面:切入点就是标记了业务操作日志注解的目标方法;切面的主要逻辑就是保存业务操作日志信息;
4.1、Spring AOP
AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向切面编程,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术,AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离;
而SpringAOP,则是AOP的一种具体实现,Spring内部对SpringAOP的应用最经典的场景就是Spring的事务,通过事务注解的配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略;与过滤器、拦截器相比,更加重要的是其适用范围不再局限于SpringMVC项目,可以在任意一层定义一个切点,织入相应的操作,并且还可以改变返回值。
4.2、SpringAOP、过滤器、拦截器对比
在匹配中同一目标时,过滤器、拦截器、SpringAOP的执行优先级是:过滤器>拦截器>SpringAOP,执行顺序是先进后出,具体的不同则体现在以下几个方面:
- 作用域不同
- 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用,对请求-响应入口处进行过滤拦截。
- 拦截器依赖于springMVC,可以在SpringMVC项目中使用,而SpringMVC的核心是DispatcherServlet,而DispatcherServlet又属于Servlet的子类,因此作用域和过滤器类似。
- SpringAOP对作用域没有限制,只要定义好切点,可以在请求-响应的入口层(controller层)拦截处理,也可以在请求的业务处理层(service层)拦截处理。
- 颗粒度的不同
- 过滤器的控制颗粒度比较粗,只能在doFilter()中对请求和响应进行过虑和拦截处理。
- 拦截器提供更精细颗粒度的控制,有preHandle()、postHandle()、afterCompletion(),可以在controller对请求处理之前、请求处理后、请求响应完毕织入一些业务操作。
- SpringAOP,提供了前置通知、后置通知、返回后通知、异常通知、环绕通知,比拦截器更加精细化的颗粒度控制,甚至可以修改返回值。
二、准备一个spring boot项目(已有的项目可跳过)
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.pzz</groupId>
<artifactId>springboot_BusLog</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--mysql mybatis druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.40</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.8</version>
</dependency>
2、配置yml
server.port=8888
spring.application.name=buslog
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/bus_log?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=zzybzb
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
3、项目的简单接口(person表)
3.1、数据库表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for person
-- ----------------------------
DROP TABLE IF EXISTS `person`;
CREATE TABLE `person` (
`id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`age` int(3) NULL DEFAULT NULL,
`addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`desc_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
3.2、entity实体
@Data
@TableName("person")
public class Person implements Serializable {
// id
private String id;
// 用户名
private String username;
// 年龄
private Integer age;
// 地址
private String addr;
// 介绍
private String descInfo;
}
3.3、controller代码
@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController {
@Autowired
private IPersonService personService;
private Integer maxCount=100;
@PostMapping
@BusLog(descrip = "添加单条人员信息")
public void add(@RequestBody Person person) {
personService.registe(person);
log.info("//增加person执行完成");
}
// @PostMapping("/batch")
// @BusLog(descrip = "批量添加人员信息")
// public String addBatch(@RequestBody List<Person> personList){
// this.personService.addBatch(personList);
// return String.valueOf(System.currentTimeMillis());
// }
//
@GetMapping
@BusLog(descrip = "人员信息列表查询")
public List<Person> list(String searchValue) {
List<Person> pageInfo = this.personService.getPersonList(searchValue);
log.info("//查询person列表执行完成");
return pageInfo;
}
@GetMapping("/{loginNo}")
@BusLog(descrip = "人员信息详情查询")
public Person info(@PathVariable String loginNo, String phoneVal) {
Person person= this.personService.getOne(loginNo);
log.info("//查询person详情执行完成");
return person;
}
@PutMapping
@BusLog(descrip = "修改人员信息")
public String edit(@RequestBody Person person) {
this.personService.update(person);
log.info("//查询person详情执行完成");
return String.valueOf(System.currentTimeMillis());
}
@DeleteMapping
@BusLog(descrip = "删除人员信息")
public String edit(@PathVariable(name = "id") Integer id) {
this.personService.delete(id);
log.info("//查询person详情执行完成");
return String.valueOf(System.currentTimeMillis());
}
}
3.4、service代码
@Service
public class IPersonServiceImpl implements IPersonService {
@Autowired
private IPersonDao iPersonDao;
/**
* 添加单个用户
* @param person
* @return
*/
@Override
public void registe(Person person) {
iPersonDao.insert(person);
}
@Override
public void update(Person person) {
iPersonDao.updateById(person);
}
@Override
public void delete(Integer id) {
iPersonDao.deleteById(id);
}
@Override
public Person getOne(String loginNo) {
return iPersonDao.selectById(loginNo);
}
@Override
public List<Person> getPersonList(String searchValue) {
QueryWrapper<Person> queryWrapper = new QueryWrapper<>();
queryWrapper.like(searchValue,new Person());
return iPersonDao.selectList(queryWrapper);
}
}
3.5、mapper代码
@Mapper
public interface IPersonDao extends BaseMapper<Person> {
}
4、启动,测试接口跑通即可
三、使用操作日志
1、需要的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、日志表
create table if not exists bus_log
(
id bigint auto_increment comment '自增id'
primary key,
bus_name varchar(100) null comment '业务名称',
bus_descrip varchar(255) null comment '业务操作描述',
oper_person varchar(100) null comment '操作人',
oper_time datetime null comment '操作时间',
ip_from varchar(50) null comment '操作来源ip',
param_file varchar(255) null comment '操作参数报文文件'
)
comment '业务操作日志' default charset ='utf8';
3、代码实现
3.1、定义业务日志注解@BusLog,可以作用在控制器或其他业务类上,用于描述当前类的功能;也可以用于方法上,用于描述当前方法的作用;
/**
* 业务日志注解
* 可以作用在控制器或其他业务类上,用于描述当前类的功能;
* 也可以用于方法上,用于描述当前方法的作用;
*/
@Target({
ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {
/**
* 功能名称
* @return
*/
String name() default "";
/**
* 功能描述
* @return
*/
String descrip() default "";
}
3.2、把业务操作日志注解BusLog标记在PersonController类和方法上。
3.3、编写切面类BusLogAop,并使用@BusLog定义切入点,在环绕通知内执行过目标方法后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述, 把方法的参数报文写入到文件中,最后保存业务操作日志信息。
@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {
@Autowired
private BusLogDao busLogDao;
/**
* 定义BusLogAop的切入点为标记@BusLog注解的方法
*/
@Pointcut(value = "@annotation(com.pzz.inter.BusLog)")
public void pointcut() {
}
/**
* 业务操作环绕通知
*
* @param proceedingJoinPoint
* @retur
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
log.info("----BusAop 环绕通知 start");
//执行目标方法
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//目标方法执行完成后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述
Object target = proceedingJoinPoint.getTarget();
Object[] args = proceedingJoinPoint.getArgs();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
BusLog anno1 = target.getClass().getAnnotation(BusLog.class);
BusLog anno2 = signature.getMethod().getAnnotation(BusLog.class);
BusLogBean busLogBean = new BusLogBean();
String logName = anno1.name();
String logDescrip = anno2.descrip();
busLogBean.setBusName(logName);
busLogBean.setBusDescrip(logDescrip);
busLogBean.setOperPerson("pzz");
busLogBean.setOperTime(new Date());
JsonMapper jsonMapper = new JsonMapper();
String json = null;
try {
json = jsonMapper.writeValueAsString(args);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//把参数报文写入到文件中
OutputStream outputStream = null;
try {
String paramFilePath = System.getProperty("user.dir") + File.separator + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";
outputStream = new FileOutputStream(paramFilePath);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
busLogBean.setParamFile(paramFilePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//保存业务操作日志信息
this.busLogDao.insert(busLogBean);
log.info("----BusAop 环绕通知 end");
return result;
}
@Override
public int getOrder() {
return 1;
}
}
3.4、entity代码和mapper代码
/**
* @author pzz
* @date 2023/6/11 22:46
*/
@Data
@TableName("bus_log")
public class BusLogBean implements Serializable {
//自增id
private Long id;
//业务名称
private String busName;
//业务操作描述
private String busDescrip;
//操作人
private String operPerson;
//操作时间
private Date operTime;
//操作来源ip
private String ipFrom;
//操作参数报文文件
private String paramFile;
}
/**
* @author pzz
* @date 2023/6/11 22:43
*/
@Mapper
public interface BusLogDao extends BaseMapper<BusLogBean> {
}
4、测试
测试工具可使用postman进行接口测试。
说明:本文参考其他博主的博客以及公众号文章,仅用于学习。
结束!!!!!!
hy:15
爱情是一种选择,而不是必然。人们不应该只因为拥有了爱情而放弃自己的自由和独立,也不应该因为没有爱情而感到自卑和孤独。