自定义注解实现数据序列化时进行数据脱敏(基于springboot默认jackjson)、消息转换器HttpMessageConverter
消息转换器 HttpMessageConverter
消息转化器的作用
将请求报文转化为Java对象
将Java对象转化为响应报文
消息转换器接口
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
转换器加载流程
消息转换器是在项目启动的时候通过WebMvcConfigurationSupport进行加载,当getMessageConverters()被调用的时候会通过configureMessageConverters()、addDefaultHttpMessageConverters()和extendMessageConverters()三个方法进行初始话消息转换器。生成的消息转换器放在List<HttpMessageConverter<?>> messageConverters集合中
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList();
// 加载委托给WebMvcConfigurer类型的Bean
this.configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
// 加载默认的转换器
this.addDefaultHttpMessageConverters(this.messageConverters);
}
// 加载扩展消息转换器
this.extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
自定义消息转换器参考下面
(1)直接注入Bean 的方式替换
(2)实现 WebMvcConfigurer#extendMessageConverters 接口方法
fastjson与jackjson
问题
在springboot中使用fastjson的@jsonField无效
原因:在springboot默认有json(jackjson)解析工具,所以使用fastjson不会生效
解决方案替换默认的解析工具(笔者不推荐,这里根据自己项目决定)
fastjson替换默认的jackjson
第一种方法bean方法
package com.anson.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class WebConfig {
/**
* @Author anson
* @Description 配置消息转换器
* @Date: 2019-12-8 11:23:33
* @version: 1.0
* new HttpMessageConverters(true, converters);
* 一定要设为true才能替换否则不会替换
* @return 返回一个消息转换的bean
*/
@Bean
public HttpMessageConverters fastJsonMessageConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
//需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//添加fastJson的配置信息;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//全局时间配置
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//在convert中添加配置信息.
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(0, fastConverter);
return new HttpMessageConverters(converters);
}
}
第二种方法实现webMvcConfigurer
@Configuration
public class WebConfigure implements WebMvcConfigurer{
/**
* 配置消息转换器
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//添加fastJson的配置信息;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//全局时间配置
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//在convert中添加配置信息.
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(0,fastConverter);
}
}
jackjson注解实现原理
参考:https://blog.csdn.net/qq_18515155/article/details/128891441?spm=1001.2014.3001.5501
这里以Jackson中常用的@JsonFormat注解为例,目标就是将Company对象序列化。
public class Company {
/**
* 开张时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private Date openDate;
public Date getOpenDate() {
return openDate;
}
public void setOpenDate(Date openDate) {
this.openDate = openDate;
}
}
public class Test {
public static void main(String[] args) {
Company company = new Company();
company.setOpenDate(new Date());
ObjectMapper objectMapper = new ObjectMapper();
String s;
try {
s = objectMapper.writeValueAsString(company);
System.out.println(s);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
执行结果如下:
{"openDate":"2023-02-05"}
处理过程简述:
寻找合适的序列化器
序列化器二次处理(主要用于传递属性元信息)
执行序列化
详细如下:
一·寻找合适的序列化器
(1)根据当前数据类型从缓存中获取对应的序列化器
(2)如果获取不到,则创建序列化器
(3)根据Company上的注解获取序列化器(Company上没有注解)
(4)根据当前的注解内省器获取能够处理 步骤3 拿到的注解的序列化器(默认的注解内省器是JacksonAnnotationIntrospector,对于注解@JsonSerialize则返回注解指定的序列化器,对于注解JsonRawValue则返回RawSerializer)
(5)如果获取不到,再根据是否实现了JsonSerializable接口或者标注了 @JsonValue注解获取对应的序列化器(当前Company都没有)
(6)如果没有,就只能创建通用的对象处理器BeanSerialzer
(7)在创建BeanSerialzer之前,需要先去找到对象的属性元信息
(8)为对象的属性(Compnay.openDate)找到合适的序列化器(类似上面的步骤,一步一步找,这里最终没有找对于openDate属性的序列化器)
(9)把属性元信息设置到对象元信息里,然后new一个BeanSerialzer并返回
(10)拿到返回的序列化器以后,如果该序列化器实现了ContextualSerializer接口,则调用它的createContextual方法进行二次处理,并返回序列化器
二.序列化器二次处理(主要用于传递属性元信息)
步骤10即为序列化器二次处理:当找到合适的序列化器以后,还需要根据是否这个序列化器实现了ContextualSerializer接口,该接口声明了方法JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
该接口可以拿到属性的元信息,如属性上的注解信息,根据元信息可以做一些二次处理,然后再返回序列化器,再用这个返回的序列化器做序列化处理。
三.执行序列化
(1)序列化对象
(2)序列化对象的字段
(3)动态获取能处理字段的序列化器
(4)检索内置的序列化器集合
(5)根据类型从内置的序列化器集合获取对应的序列化器,即根据openDate的类型Date.class获取到DateSerializer
(6)序列化器二次处理
(7)设置需要传递到序列化器中的上下文信息
(8)将openDate上的JsonFormat注解的pattern字段值传递到DateSerializer,用来在序列化openDate时知道应该转为哪种时间格式
(9)调用DateSerializer.serialze进行字段序列化
jackjson实现数据脱敏
脱敏即是对数据的部分信息用脱敏符号(*)处理。
自定义注解:
package com.chinatower.platform.awh.framework;
import com.chinatower.platform.awh.framework.enums.SentitiveFieldEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveField {
SentitiveFieldEnum type();
}
枚举类:
package com.chinatower.platform.awh.framework.enums;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public enum SentitiveFieldEnum {
/**
* 身份证号
*/
ID_CARD,
/**
* 姓名
*/
NAME,
/**
* 手机号
*/
PHONE,
}
工具类:
package com.chinatower.platform.awh.framework.config;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.lang3.StringUtils;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveDataUtils {
public static String encryptCard(String card){
String left = StringUtils.left(card, 1);
return StringUtils.rightPad(left,card.length(),"*");
}
public static String encryptPhone(String card){
String left = StringUtils.left(card, 2);
return StringUtils.rightPad(left,card.length(),"*");
}
public static String encryptName(String card){
String left = StringUtils.left(card, 3);
return StringUtils.rightPad(left,card.length(),"*");
}
}
自定义处理注解的序列化器
package com.chinatower.platform.awh.framework.config;
import com.chinatower.platform.awh.framework.SensitiveField;
import com.chinatower.platform.awh.framework.enums.SentitiveFieldEnum;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveFieldSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SentitiveFieldEnum eum ;
public SensitiveFieldSerializer(){}
public SensitiveFieldSerializer(SentitiveFieldEnum enu){this.eum = enu;}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SentitiveFieldEnum sentitiveFieldEnum = eum;
if (sentitiveFieldEnum == null){
return;
}
switch (sentitiveFieldEnum){
case ID_CARD:
s=SensitiveDataUtils.encryptCard(s);
break;
case NAME:
s=SensitiveDataUtils.encryptName(s);
break;
case PHONE:
s=SensitiveDataUtils.encryptPhone(s);
break;
default:break;
}
jsonGenerator.writeObject(s);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
//获取注解
System.out.println(1);
if (beanProperty != null){
SensitiveField annotation = beanProperty.getAnnotation(SensitiveField.class);
SentitiveFieldEnum type = annotation.type();
return new SensitiveFieldSerializer(type);
}
//返回处理枚举
System.out.println(2);
return serializerProvider.findNullValueSerializer(null);
}
}
自定义注解内省器
package com.chinatower.platform.awh.framework;
import com.chinatower.platform.awh.framework.config.SensitiveFieldSerializer;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveFieldAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
SensitiveField annotation = am.getAnnotation(SensitiveField.class);
if (annotation !=null){
return SensitiveFieldSerializer.class;
}
return super.findSerializer(am);
}
}
Jackson序列化配置
package com.chinatower.platform.awh.framework.config;
import com.chinatower.platform.awh.framework.SensitiveFieldAnnotationIntrospector;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
@Configuration
public class JacksonConfig {
@Bean
public SensitiveFieldAnnotationIntrospector sensitiveFieldAnnotationIntrospector(){
return new SensitiveFieldAnnotationIntrospector();
}
@Bean
public SensitiveFieldSerializer sensitiveFieldSerializer(){
return new SensitiveFieldSerializer();
}
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector){
ObjectMapper mapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = mapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector pair = AnnotationIntrospector.pair(annotationIntrospector, introspector);
mapper.setAnnotationIntrospector(pair);
return mapper;
}
}
springboot的bean是单例模式
测试
/ 定义一个对象,字段标有注解
@Data
public class User {
@SensitiveField(type = SensitiveFieldTypeEnum.NAME)
private String name;
@SensitiveField(type = SensitiveFieldTypeEnum.PHONE)
private String phone;
private Integer age;
}
@GetMapping("/get")
public User get() {
User user = new User();
user.setAge(20);
user.setName("徐小莫");
user.setPhone("18211843612");
return user;
}
执行结果:
{
"name": "***",
"phone": "182****3672",
"age": 20
}
代码分析:
(1)如何找到处理自定义注解的序列化器
就要通过注解內省器找到合适的序列化器。
所以我们自定义了一个注解內省器,并把它加入到了当前Jackson注解內省器集合里:
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
(2)**如何不影响已有的Jackson序列化配置 **
实际的SpringBoot项目中,对于Controller的返回数据,经常会在配置文件或者配置类进行一些Jackson的自定义配置。
如果针对当前的自定义注解序列化器新建一个ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
SensitiveFieldSerializer sensitiveFieldSerializer = new SensitiveFieldSerializer();
simpleModule.addSerializer(String.class, sensitiveFieldSerializer);
objectMapper.registerModule(simpleModule);
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newIntro = AnnotationIntrospectorPair.pair(annotationIntrospector,new SensitiveFieldAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newIntro);
那这个ObjectMapper不会包含原来的那些配置,因为他是一个新的对象。
要保证原来的配置生效,就需要对SpringBoot自动配置的ObjectMapper动手脚。
通过分析JacksonAutoConfiguration.class这个自动配置类
@Bean
JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}
@Bean
@Scope("prototype")
//这个注解是多例模式
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
this.customize(builder, customizers);
return builder;
}
@Bean
@Primary
@ConditionalOnMissingBean
// 这个注解代表没有自定义的ObjectMqpper bean对象时才会在这里创建
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
发现SpringBoot会将配置信息封装成Jackson2ObjectMapperBuilder,进而去创建ObjectMapper对象。
所以需要通过注入已有的Jackson2ObjectMapperBuilder对象来创建我们自己的ObjectMapper,再将自定义注解内省器加入到它的內省器集合,使我们的自定义注解序列化器生效:
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
return objectMapper;
}
自定义注解实现数据序列化时进行数据脱敏(基于springboot默认jackjson)、消息转换器HttpMessageConverter的更多相关文章
- ASP.Net中关于WebAPI与Ajax进行跨域数据交互时Cookies数据的传递
本文主要介绍了ASP.Net WebAPI与Ajax进行跨域数据交互时Cookies数据传递的相关知识.具有很好的参考价值.下面跟着小编一起来看下吧 前言 最近公司项目进行架构调整,由原来的三层架构改 ...
- 自定义JsonResult处理JSON序列化DateTime类型数据(Ext4.2+ASP.NET MVC 4)
最近项目中前台页面使用Extjs4.2 ,在后台ASP.NET MVC4返回的DateTime类型的数据错返回的DateTime类型的JsonResult的结果中的值是“\/Date(13784461 ...
- ASP.Net WebAPI与Ajax进行跨域数据交互时Cookies数据的传递
前言 最近公司项目进行架构调整,由原来的三层架构改进升级到微服务架构(准确的说是服务化,还没完全做到微的程度,颗粒度没那么细),遵循RESTFull规范,使前后端完全分离,实现大前端思想.由于是初次尝 ...
- SpringMVC源码剖析5:消息转换器HttpMessageConverter与@ResponseBody注解
转自 SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Spring源码 ...
- 用自定义注解实现fastjson序列化的扩展
这篇文章起源于项目中一个特殊的需求.由于目前的开发方式是前后端分离的,基本上是通过接口提供各个服务. 而前两天前端fe在开发中遇到了一些问题:他们在处理字符串类型的时间时会出现精度丢失的情况,所以希望 ...
- [大牛翻译系列]Hadoop(16)MapReduce 性能调优:优化数据序列化
6.4.6 优化数据序列化 如何存储和传输数据对性能有很大的影响.在这部分将介绍数据序列化的最佳实践,从Hadoop中榨出最大的性能. 压缩压缩是Hadoop优化的重要部分.通过压缩可以减少作业输出数 ...
- 使用Spring Aop自定义注解实现自动记录日志
百度加自己琢磨,以下亲测有效,所以写下来记录,也方便自己回顾浏览加深印象之类,有什么问题可以评论一起解决,不完整之处也请大佬指正,一起进步哈哈(1)首先配置文件: <!-- 声明自动为sprin ...
- Java如何自定义注解
本文主要是记录所学,以供后续参考.注解是Java 1.5引入的,Java自定义注解是通过运行时靠反射获取注解,注解相当于是一种嵌入在程序中的元数据,可以使用注解解析工具或编译器对其进行解析,也可以指定 ...
- SpringMVC日志管理(自定义异常及自定义注解)
近期为了规范公司老旧项目的日志,主要也是为了便于日后错误排查以及加强对业务系统的监控,准备对原有的日志输出进行简单的改造. 解决思路 1.通过自定义异常来将可能出现的问题分为两大类,业务类及系统类.同 ...
- jmeter 导入csv数据中json格式数据取值不完整
1.jmeter中添加csv数据文件时,数据是json格式 2.jmeter中执行取值发现只取了一部分 分析原因,json格式数据,中间有逗号,而csv是根据逗号来分割的,这回导致我们取值错位. 解决 ...
随机推荐
- 更换Mysql数据库-----基于Abo.io 的书籍管理Web应用程序
之前公司一直使用的是ASP.NET Boilerplate (ABP),但是当解决方案变得很大时,项目启动就变得非常慢,虽然也想了一些办法,将一些基础模块做成Nuget包的形式,让整个解决方案去引用. ...
- 10.1. Java性能调优
Java性能调优是一个复杂且重要的主题,它涉及到了JVM.垃圾收集器.内存管理.多线程.代码优化等多个方面.在本节中,我们将对Java性能调优的基本概念和方法进行简要介绍. 10.1.1. 理解性能指 ...
- CANoe_系统变量的创建过程
在Canoe中创建系统变量,可以用于定义和管理与CAN网络通信相关的参数和配置.遵循以下步骤: 1.打开Canoe 启动Canoe软件. 2.打开项目 在Canoe的菜单栏中,选择"File ...
- WPF 入门笔记 - 03 - 样式基础
程序的本质 - 数据结构 + 算法 本篇为学习李应保老师所著的<WPF专业编程指南>并搭配WPF开发圣经<WPF编程宝典第4版>以及痕迹大佬<WPF入门基础教程系列> ...
- Java 网络编程 —— RMI 框架
概述 RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都 ...
- 高效处理报表,掌握原生JS打印和导出报表为PDF的顺畅技巧!
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言篇 在日常工作中,报表打印和导出为PDF是经常要处理的任务 ...
- Linux下C语言实现贪吃蛇
简单记录下贪吃蛇小游戏. 以下是源码: 1 #include <curses.h> 2 #include <stdlib.h> 3 #include <pthread.h ...
- 什么是ORM (object real mapping)
一.ORM简介 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术.简单的说,ORM是通过使用 ...
- gitlab配置环境及pycharm配置
一.gitlab介绍 GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务 git.gitlab.GitHub的简单区别 git 是一种基于命令 ...
- Unity的Console的控制类LogEntries:深入解析与实用案例
使用Unity Console窗口的LogEntries私有类实现自定义日志系统 在Unity开发过程中,我们经常需要使用Console窗口来查看程序运行时的日志信息.Unity内置的日志系统提供了基 ...