针对这个需求,我建议你可以通过以下方式来解决:
1. 在数据库中为每一张数据表添加两张表,一张用于存储原始数据,一张用于存储变更历史记录。例如:我们有表A,那么我们为表A创建A_Orignal和A_ChangeLog表。
2. 在A_Orignal表中对应需要记录的字段增加记录时间(create_time)和更新时间(update_time)两个字段。当插入或更新数据时,将当前的时间戳记录到create_time和update_time中。
3. 当发生数据更新时,通过对比A_Orignal表中的和新值,筛选出修改了的字段并记录在A_ChangeLog表中。例如,我们可以设计A_ChangeLog为id, ref_id, field_name, old_value, new_value, change_time,其中ref_id为A_Orignal表中的主键,即记录的是哪个数据的修改历史。field_name记录修改的字段名,old_value记录旧值,new_value记录新值。change_time记录修改的时间。
4. 当需要查询某条数据的修改历史时,只需要根据主键id,在A_ChangeLog表中查询出对应该条记录在何时修改了哪些字段即可。
5. 如果需要查询历史记录的一些统计信息,例如当天有多少条数据被修改了,某个字段在一段时间内被修改的次数等,可以通过SQL语句来实现。
复用性的解决方案,给我一个范例,方案如下:框架是springboot,写一个自定义注解,然后去拦截所有打到自定义注解的方法,在保存之前查询原值,保存之后把原值和新值做对比,最后去保存这个变更记录
以下是一个范例:
- 首先,我们在项目的代码中创建一个自定义注解@MyLog,用于标注需要记录操作日志的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
String value() default "";
}
- 创建一个拦截器实现类LogInterceptor, 拦截所有打了@MyLog注解的方法,在方法执行之前和之后进行操作,获取要记录日志的类、方法名称和参数,以及执行前后的数据变化信息,并将这些信息保存到日志表中。
@Aspect
@Component
public class LogInterceptor {
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
private ThreadLocal<Long> time = new ThreadLocal<>();
@Autowired
private LogService logService;
@Pointcut("@annotation(com.example.demo.annotation.MyLog)")
public void pointcut() {}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
time.set(System.currentTimeMillis());
logger.info("================== Start ==================");
logger.info("Class Method : {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "pointcut()")
public void afterReturning(Object ret) {
logger.info("Response : {}", ret);
logger.info("================== End, Time Used: {}ms ==================", System.currentTimeMillis()-time.get());
}
@AfterThrowing(pointcut = "pointcut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
logger.info("================== Error ==================");
logger.info("Class Method : {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
logger.error("Error Message : {}", e.getMessage());
logger.info("================== End ==================");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) {
try {
long startTime = System.currentTimeMillis();
Object result = pjp.proceed();
long elapsedTime = System.currentTimeMillis() - startTime;
saveLog(pjp, elapsedTime, result);
return result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private void saveLog(ProceedingJoinPoint jp, long elapsedTime, Object result) throws Exception {
MethodSignature signature = (MethodSignature) jp.getSignature();
String methodName = signature.getName();
String className = jp.getTarget().getClass().getSimpleName();
Object[] args = jp.getArgs();
// 获得业务实体类型
if (args.length > 0) {
Class<?> clazz = args[0].getClass();
// 查询原始数据
String originalData = "";
String primaryKey = "";
// 这里需要根据实际业务情况查询原始数据, 例如:
// primaryKey = ((Entity)args[0]).getId();
// originalData = queryOriginalData(primaryKey, clazz);
// 执行新数据
String newData = JSON.toJSONString(args[0]);
// 记录操作日志
LogEntity logEntity = new LogEntity(className, methodName, originalData, newData, primaryKey);
logEntity.setTime(new Date());
// 这里需要保存记录到数据库表
// logService.save(logEntity);
}
}
}
- 在需要记录日志的方法前加上@MyLog注解即可。
@MyLog
public void update(User user) {
// 更新用户信息到数据库表
userDao.update(user);
}
这个方案的实现:在AOP的方法拦截器中,获取到请求的类名、方法名称和参数列表。在执行业务前,从数据库中查询实体原始数据,在执行业务后,获取实体修改后的数据,并将它们进行对比,记录变更历史并保存到数据库表中。
如果我在业务数据保存成功了,但是在对比或者保存变更记录的时候出现了异常,这个时候怎么办要不要进行重试,要不要进行回滚?
如果在保存变更记录时出现异常,可以尝试进行重试操作,如果重试多次后还是无法保存成功,建议进行回滚操作。具体的实现方法可以通过try-catch块加事务来实现。以下是一个简单的相关代码示例:
@Service
@Transactional(rollbackFor = Exception.class)
public class LogService {
@Autowired
private LogDao logDao;
public void save(LogEntity logEntity) {
int retryTimes = 3;
boolean isSuccess = false;
Exception exception = null;
for (int i = 0; i < retryTimes; i++) {
try {
logDao.save(logEntity);
isSuccess = true;
break;
} catch (Exception e) {
logger.error("Save log failed, retry: {}, error message: {}", i+1, e.getMessage());
exception = e;
}
}
if (!isSuccess) {
throw new RuntimeException("Save log failed after retrying " + retryTimes + " times.", exception);
}
}
}
在这个示例中,我们定义了一个重试次数的变量retryTimes,然后在循环中进行多次保存操作。如果保存成功了,直接退出循环;如果保存失败,则记录错误信息,进行重试操作,重试操作未成功,则抛出运行时异常并回滚事务。
==============================================================
小知识
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
这个属性什么意思,有什么作用?有用吗
这个属性定义了一个静态Logger对象,用于记录系统日志信息。Logger对象是Slf4j框架提供的,可以使用其它日志框架的实现(如Logback、Log4j)。
在代码中使用Logger对象可以记录系统的日志信息,例如记录系统错误信息、调试信息等,以便于后续的查看和分析。通常我们会将不同的日志信息记录到不同的文件中,以便于独立维护和查看。另外,Slf4j框架可以根据不同的日志输出级别(如调试、信息、警告、错误)来对日志信息进行分类输出,从而方便程序员进行日志分析和问题排查。
在开发中,Logger对象是非常常用的,可以对系统运行状态进行全面监控和评估,对于应用程序的稳定性、性能优化以及问题排查都非常有用。需要注意的是,在记录日志信息时,应该遵循日志级别化、减少日志量、格式统一等良好习惯,以提高日志信息的可读性和性能。
记录日志通常使用Logger的不同方法,可以根据级别的不同选择不同的方法,常用的方法有:
- logger.trace(String msg): 记录跟踪信息,一般用于诊断问题,这个级别的日志一般不会送入生产环境。
- logger.debug(String msg): 记录调试信息,记录一些运行时的调试信息,一般用于开发和测试阶段,正式发布的版本中这个日志级别一般不会记录。
- logger.info(String msg): 记录一般性信息,记录一些非错误性质的信息,如系统启动信息、访问日志等。
- logger.warn(String msg): 记录警告信息,表示可能会出现一些问题,但并不影响程序的运行,可以通过修改程序避免出现更严重的问题。
- logger.error(String msg): 记录错误信息,表示出现了一些错误,需要相关人员进一步排查问题。
下面给出一个范例:
public class DemoController {
private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
@RequestMapping("/demo")
public String demo() {
logger.info("进入demo方法");
try {
// do something
logger.info("执行成功,返回结果");
return "success";
} catch (Exception e) {
logger.error("执行失败,抛出异常", e);
return "error";
}
}
}
在这个例子中,我们在DemoController中定义了一个静态Logger对象,并在demo方法中记录了info和error级别的日志信息。logger.info方法记录了“进入demo方法”和“执行成功,返回结果”两个信息,表示进入方法和方法结束时的状态;logger.error方法记录了“执行失败,抛出异常”,并将异常信息e作为参数输出,表示方法执行发生错误的原因。通过这种方式,我们可以了解系统的运行状态,以便于进行调试和排查问题。
这个记录我可以通过属性在程序里面调出来打印吗?
可以,通过设置日志级别和输出目标,可以将日志信息输出到不同的地方,如控制台、文件、数据库等。Slf4j框架提供了多种输出日志信息的方式,常用的有以下几种:
- 控制台输出
将日志信息输出到控制台,可以通过以下配置来实现:
# 配置日志级别
logging.level.com.example=info
# 配置控制台输出
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) [%thread] %logger{50} - %msg%n
这里的配置是基于Logback实现的,logging.pattern.console表示控制台输出的格式,%d、%highlight、%level等是占位符,详细的占位符可以在Logback的官网上查看。
- 文件输出
将日志信息输出到文件中,可以通过以下配置来实现:
# 配置日志级别
logging.level.com.example=info
# 配置文件输出
logging.file.name=mylog.log
logging.file.path=/var/log/myapp
这个配置表示将日志信息输出到/var/log/myapp/mylog.log文件中。
- 使用注解输出日志
在类或方法上添加注解,使其在运行时输出日志信息,可以通过以下方式实现:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/demo")
public class DemoController {
private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
@RequestMapping("/hello")
@Loggable(LogLevel.INFO) // 添加注解
public String hello() {
logger.info("进入hello方法");
return "hello";
}
}
在这里,我们定义了一个@Loggable注解,并在DemoController的hello方法上添加该注解,当运行到这个方法时,会输出日志信息。@Loggable注解的实现可以参考相关的框架或自己编写。
我可以将日志以json形式打给前端吗?
重试
复制
可以,将日志以JSON格式输出,可以更方便地让前端解析和展示日志信息。在Java中,需要引入Jackson或Gson等JSON处理库,将日志信息转换为JSON格式后输出即可。
以下是一个使用Jackson的示例:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log")
public class LogController {
private static final Logger logger = LoggerFactory.getLogger(LogController.class);
@GetMapping("/json")
public String jsonLog() throws JsonProcessingException {
// 模拟系统运行时产生的日志信息
logger.info("系统运行正常");
logger.warn("警告信息:磁盘空间不足");
logger.error("错误信息:数据读取失败");
// 将日志信息转换为JSON格式输出
ObjectMapper mapper = new ObjectMapper();
String jsonLog = mapper.writeValueAsString(logger);
return jsonLog;
}
}
这个例子中我们定义了一个LogController来处理日志相关的请求,其中的jsonLog方法将系统运行时的日志信息转换为JSON格式的字符串并返回给前端。需要注意的是,在实际生产中,为了避免日志信息过大导致前端无法正常显示,可以根据机器性能和网络带宽等因素调整日志输出的级别和格式。