Spring Boot框架中使用Jackson的处理总结

1.前言

通常我们在使用Spring Boot框架时,如果没有特别指定接口的序列化类型,则会使用Spring Boot框架默认集成的Jackson框架进行处理,通过Jackson框架将服务端响应的数据序列化成JSON格式的数据。

本文主要针对在Spring Boot框架中使用Jackson进行处理的经验进行总结,同时也结合在实际开发场景中碰到的问题以及解决方案进行陈述。

本文涉及到的源码地址:https://gitee.com/dt_research_institute/code-in-action

PS:目前市面上针对JSON序列化的框架很多,比较出名的就是Jackson、Gson、FastJson。如果开发者对序列化框架没有特别的要求的情况下,个人建议是直接使用Spring Boot框架默认集成的Jackson,没有必要进行更换。

2.统一序列化时间格式

在我们的接口中,针对时间类型的字段序列化是最常见的需求之一,一般前后端开发人员会针对时间字段统一进行约束,这样有助于在编码开发时,统一编码规范。

在Spring Boot框架中,如果使用Jackson处理框架,并且没有任何配置的情况下,Jackson针对不同时间类型字段,序列化的格式也会不尽相同。

先来看一个简单示例,User.java实体类编码如下:

public class User {private String name;private Integer age;private LocalDateTime birthday;private Date studyDate;private LocalDate workDate;private Calendar firstWorkDate;public static User buildOne(){User user=new User();LocalDateTime now=LocalDateTime.now();user.setWorkDate(now.plusYears(25).toLocalDate());user.setStudyDate(Date.from(now.plusYears(5).atZone(ZoneId.systemDefault()).toInstant()));user.setName("姓名-"+RandomUtil.randomString(5));user.setAge(RandomUtil.randomInt(0,100));user.setBirthday(now);user.setFirstWorkDate(Calendar.getInstance());return user;}//getter and setter...
}

接口代码层也很简单,返回一个User的实体对象即可,代码如下:

@RestController
public class UserApplication {@GetMapping("/queryOne")public ResponseEntity<User> queryOne(){return ResponseEntity.ok(User.buildOne());}
}

如果我们对框架代码没有任何的配置,此时我们通过调用接口/queryOne,拿到的返回结果数据如下图:

image-20210312085839202

Jackson序列化框架针对四个不同的时间类型字段,序列化处理的操作是不同的,如果我们对时间字段有格式化的要求时,我们应该如何处理呢?

2.1 通过@JsonFormat注解

最直接也是最简单的一种方式,是我们通过使用Jackson提供的@JsonFormat注解,对需要格式化处理的时间字段进行标注,在@JsonFormat注解中写上我们的时间格式化字符,User.java代码如下:

public class User {private String name;private Integer age;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime birthday;private Date studyDate;private LocalDate workDate;private Calendar firstWorkDate;//getter and setter...
}

此时,我们再通过调用接口,拿到的返回结果如下图:

image-20210312090417967

通过对birthday字段标注@JsonFormat注解,最终Jackson框架会将该字段序列化为我们标注的格式类型。

2.2 配置全局application.yml

通过@JsonFormat注解的方式虽然能解决问题,但是我们在实际的开发当中,涉及到的时间字段会非常多,如果全部都用注解的方式对项目中的时间字段进行标注,那开发的工作量也会很大,并且多团队一起协同编码时,难免会存在遗漏的情况,因此,@JsonFormat注解只适用于针对特定的接口,特定的场景下,对序列化响应的时间字段进行约束,而在全局的角度来看,开发者应该考虑通过在application.yml配置文件中进行全局配置

针对Spring Boot框架中Jackson的全局配置,我们在application.yml进行配置时,IDEA等编辑器会给出相应的提示,包含的属性如下图:

image-20210312092003557

开发者可以通过org.springframework.boot.autoconfigure.jackson.JacksonProperties.java查看所有配置的源码信息

配置属性说明
date-format日期字段格式化,例如:yyyy-MM-dd HH:mm:ss

针对日期字段的格式化处理,我们只需要使用date-format属性进行配置即可,application.yml配置如下:

spring:jackson:date-format: yyyy-MM-dd HH:mm:ss

当然,如果有必要的话,还需要配置time-zone时区属性,不过该属性不配置的情况下,Jackson会使用系统默认时区。

我们从Spring Boot的源码中可以看到对Jackson的时间处理逻辑,JacksonAutoConfiguration.java中部分代码如下:

private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {// We support a fully qualified class name extending DateFormat or a date// pattern string valueString dateFormat = this.jacksonProperties.getDateFormat();if (dateFormat != null) {try {Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null);builder.dateFormat((DateFormat) BeanUtils.instantiateClass(dateFormatClass));}catch (ClassNotFoundException ex) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);// Since Jackson 2.6.3 we always need to set a TimeZone (see// gh-4170). If none in our properties fallback to the Jackson's// defaultTimeZone timeZone = this.jacksonProperties.getTimeZone();if (timeZone == null) {timeZone = new ObjectMapper().getSerializationConfig().getTimeZone();}simpleDateFormat.setTimeZone(timeZone);builder.dateFormat(simpleDateFormat);}}
}

从上面的代码中,我们可以看到的处理逻辑:

  • 从yml配置文件中拿到dateFormat属性字段
  • 首先通过ClassUtils.forName方法来判断开发者配置的是否是格式化类,如果配置的是格式化类,则直接配置dateFormat属性
  • 类找不到的情况下,捕获ClassNotFoundException异常,默认使用JDK自带的SimpleDateFormat类进行初始化

最终,我们在application.yml配置文件中配置了全局的Jackson针对日期处理的格式化信息,此时我们再看/queryOne接口响应的内容是什么情况呢?如下图:

image-20210312094014588

从图中我们可以发现,除了LocalDate类型的字段,包含时分秒类型的日期类型:LocalDateTimeDateCalendar全部按照我们的要求将日期序列化成了yyyy-MM-dd HH:mm:ss格式,达到了我们的要求。

3.Jackson在Spring Boot框架中的配置选项

在上面的时间字段序列化处理,我们已经知道了如何配置,那么在Spring Boot的框架中,针对Jackson的各个配置项主要包含哪些呢?我们通过IDEA的提示可以看到,配置如下图:

image-20210312092003557

在上面的12个属性中,每个属性的配置都会对Jackson产生不同的效果,接下来,我们逐一详解每个属性配置的作用

3.1 date-format日期格式化

date-format在前面我们已经知道了该属性的作用,主要是针对日期字段的格式化

3.2 time-zone时区

time-zone字段也是和日期字段类型,使用不同的时区,最终日期类型字段响应的结果会不一样

时区的表示方法有两种:

  • 指定时区的名称,例如:Asia/Shanghai,America/Los_Angeles
  • 通过格林威治平时GMT针对时分秒做+或者-自定义操作

通过指定时区的名称,假设我们指定当前的项目是America/Los_Angeles,那么接口响应的数据是什么效果呢?

PS:时区名称如果不是很清楚的话,一般在Linux服务器的/usr/share/zoneinfo目录可以进行查看,如下图:

image-20210312131802521

application.yml:

spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: America/Los_Angeles

效果图如下:

image-20210312130547087

我们在结合代码来分析:

//User.java
public static User buildOne(){User user=new User();LocalDateTime now=LocalDateTime.now();user.setWorkDate(now.plusYears(25).toLocalDate());user.setStudyDate(Date.from(now.plusYears(5).atZone(ZoneId.systemDefault()).toInstant()));user.setName("姓名-"+RandomUtil.randomString(5));user.setAge(RandomUtil.randomInt(0,100));user.setBirthday(now);user.setFirstWorkDate(Calendar.getInstance());return user;
}

由于洛杉矶时区与上海时区相差16个小时,因此,Jackson框架针对日期的序列化时,分别做了不同类型的处理,但我们也能看出差别

  • LocalDateTimeLocalDate类型的字段,Jackson的时区设置不会对该字段产生影响(因为这两个日期类型自带时区属性)
  • DateCalendar类型的字段受Jackson序列化框架的时区设置影响

另外一种方式是通过格林威治平时(GMT)做加减法,主要有两种格式支持:

  • GMT+HHMM或者GMT-HHMM或者GMT+H:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,例如我们常见的GMT+8代表东八区,也就是北京时间
  • GMT+HH:MM或者GMT-HH:MM:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,和上面意思差不多

可以自己写测试代码进行测试,示例如下:

public class TimeTest {public static void main(String[] args) {LocalDateTime localDateTime=LocalDateTime.now();DateTimeFormatter dateTimeFormatter=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");System.out.println(localDateTime.format(dateTimeFormatter));System.out.println(LocalDateTime.now(ZoneId.of("GMT+0901")).format(dateTimeFormatter));System.out.println(LocalDateTime.now(ZoneId.of("GMT+09:01")).format(dateTimeFormatter));}
}

3.3 locale本地化

JSON序列化时Locale的变量设置

3.4 visibility访问级别

Jackson支持从私有字段中读取值,但是默认情况下不这样做,如果我们的项目中存在不同的序列化反序列化需求,那么我们可以在配置文件中对visibility进行配置

我们将上面User.java代码中的name属性的get方法修饰符从public变更为private,其他字段保持不变

代码如下:

public class User {private String name;private Integer age;private Date nowDate;private LocalDateTime birthday;private Date studyDate;private LocalDate workDate;private Calendar firstWorkDate;//getter方法修饰符从public修改为privateprivate String getName() {return name;}//other setter and getter
}

此时,我们通过调用/queryOne接口响应结果如下:

image-20210314191124147

从结果中我们可以看到,由于我们将name属性的getter方法设置为了private,因此jackson在序列化时,没有拿到该字段

此时,我们再修改application.yml的配置,如下:

spring:jackson:visibility:getter: any

我们通过将getter设置为any级别的类型,再调用/queryOne接口,响应结果如下:

image-20210314191405490

从图中可以看出,jackson序列化结果中又出现了name属性,这代表即使name字段的属性和getter方法都是private,但是jackson还是获取到了该成员变量的值,并且进行了序列化处理。

通过设置visibility属性即可达到上面的效果。开发者根据自己的需要自行进行选择。

3.5 property-naming-strategy属性命名策略

通常比较常见的我们针对java代码中的实体类属性一般都是驼峰命名法(Camel-Case),但是Jackson序列化框架也提供了更多的序列化策略,而property-naming-strategy就是配置该属性的。

先来看Spring Boot框架如何配置jackson的命名策略

JacksonAutoConfiguration.java
private void configurePropertyNamingStrategyField(Jackson2ObjectMapperBuilder builder, String fieldName) {// Find the field (this way we automatically support new constants// that may be added by Jackson in the future)Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, fieldName,PropertyNamingStrategy.class);Assert.notNull(field, () -> "Constant named '" + fieldName + "' not found on "+ PropertyNamingStrategy.class.getName());try {builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null));}catch (Exception ex) {throw new IllegalStateException(ex);}
}

通过反射,直接获取PropertyNamingStrategy类中的成员变量的值

PropertyNamingStrategy定义了Jackson(2.11.4)框架中的命名策略常量成员变量

package com.fasterxml.jackson.databind;//other importpublic class PropertyNamingStrategy // NOTE: was abstract until 2.7implements java.io.Serializable
{/*** Naming convention used in languages like C, where words are in lower-case* letters, separated by underscores.* See {@link SnakeCaseStrategy} for details.** @since 2.7 (was formerly called {@link #CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES})*/public static final PropertyNamingStrategy SNAKE_CASE = new SnakeCaseStrategy();/*** Naming convention used in languages like Pascal, where words are capitalized* and no separator is used between words.* See {@link PascalCaseStrategy} for details.** @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})*/public static final PropertyNamingStrategy UPPER_CAMEL_CASE = new UpperCamelCaseStrategy();/*** Naming convention used in Java, where words other than first are capitalized* and no separator is used between words. Since this is the native Java naming convention,* naming strategy will not do any transformation between names in data (JSON) and* POJOS.** @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})*/public static final PropertyNamingStrategy LOWER_CAMEL_CASE = new PropertyNamingStrategy();/*** Naming convention in which all words of the logical name are in lower case, and* no separator is used between words.* See {@link LowerCaseStrategy} for details.* * @since 2.4*/public static final PropertyNamingStrategy LOWER_CASE = new LowerCaseStrategy();/*** Naming convention used in languages like Lisp, where words are in lower-case* letters, separated by hyphens.* See {@link KebabCaseStrategy} for details.* * @since 2.7*/public static final PropertyNamingStrategy KEBAB_CASE = new KebabCaseStrategy();/*** Naming convention widely used as configuration properties name, where words are in* lower-case letters, separated by dots.* See {@link LowerDotCaseStrategy} for details.** @since 2.10*/public static final PropertyNamingStrategy LOWER_DOT_CASE = new LowerDotCaseStrategy();//others...
}

从源码中我们可以看到,有六种策略供我们进行配置,配置示例如下:

spring:jackson:date-format: yyyy-MM-dd HH:mm:sslocale: zh_CNtime-zone: GMT+8visibility:getter: anyproperty-naming-strategy: LOWER_CAMEL_CASE

SNAKE_CASE

SNAKE_CASE主要包含的规则,详见SnakeCaseStrategy:

  • java属性名称中所有大写的字符都会转换为两个字符,下划线和该字符的小写形式,例如userName会转换为user_name,对于连续性的大写字符,近第一个进行下划线转换,后面的大小字符则是小写,例如theWWW会转换为the_www
  • 对于首字母大写的情况,近转成小写,例如:Results会转换为results,并不会转换为_results
  • 针对属性中已经包含下划线的情况,仅做小写转换处理
  • 下划线出现在首位的情况下,会被去除处理,例如属性名:_user会被转换为user

真实效果如下图:

image-20210315125556270

UPPER_CAMEL_CASE

UPPER_CAMEL_CASE顾名思义,驼峰命名法的规则,只是首字母会转换为大写,详见UpperCamelCaseStrategy

真实效果图如下:

image-20210315131430536

LOWER_CAMEL_CASE

LOWER_CAMEL_CASE效果和UPPER_CAMEL_CASE正好相反,其首字母会变成小写,详见LowerCamelCaseStrategy

效果图如下:

image-20210315131729151

LOWER_CASE

LOWER_CASE从命名来看很明显,将属性名 全部转为小写,详见LowerCaseStrategy

KEBAB_CASE

KEBAB_CASE策略和SNAKE_CASE规则类似,只是下划线变成了横线-,详见KebabCaseStrategy

效果图如下:

image-20210315132557384

LOWER_DOT_CASE

LOWER_DOT_CASE策略和KEBAB_CASE规则相似,只是由横线变成了点.,详见LowerDotCaseStrategy

效果图如下:

image-20210315132720262

总结:看了上面这么多属性名称的策略,其实每一种类型只是不同的场景下才需要,如果上面jackson给定的默认策略名称无法满足,我们从源码中也能看到,通过自定义实现类,也能满足企业的个性化需求,非常方便。

3.6 mapper通用功能开关配置

mapper属性是一个Map类型,主要是针对MapperFeature定义开关属性,是否启用这些特性

/**
* Jackson general purpose on/off features.
*/
private final Map<MapperFeature, Boolean> mapper = new EnumMap<>(MapperFeature.class);

MapperFeature.java中,我们可以跟踪源码来看:

/*** Enumeration that defines simple on/off features to set* for {@link ObjectMapper}, and accessible (but not changeable)* via {@link ObjectReader} and {@link ObjectWriter} (as well as* through various convenience methods through context objects).*<p>* Note that in addition to being only mutable via {@link ObjectMapper},* changes only take effect when done <b>before any serialization or* deserialization</b> calls -- that is, caller must follow* "configure-then-use" pattern.*/
public enum MapperFeature implements ConfigFeature
{//.......
}

MapperFeature是一个枚举类型,对当前jackson的一些特性通过枚举变量的方式来定义开关属性,也是方便使用者来使用的。

主要包含以下枚举变量:

  • USE_ANNOTATIONS:
  • USE_GETTERS_AS_SETTERS
  • PROPAGATE_TRANSIENT_MARKER
  • AUTO_DETECT_CREATORS
  • AUTO_DETECT_FIELDS
  • AUTO_DETECT_GETTERS
  • AUTO_DETECT_IS_GETTERS
  • AUTO_DETECT_SETTERS
  • REQUIRE_SETTERS_FOR_GETTERS
  • ALLOW_FINAL_FIELDS_AS_MUTATORS
  • INFER_PROPERTY_MUTATORS
  • INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES
  • CAN_OVERRIDE_ACCESS_MODIFIERS
  • OVERRIDE_PUBLIC_ACCESS_MODIFIERS
  • USE_STATIC_TYPING
  • USE_BASE_TYPE_AS_DEFAULT_IMPL
  • DEFAULT_VIEW_INCLUSION
  • SORT_PROPERTIES_ALPHABETICALLY
  • ACCEPT_CASE_INSENSITIVE_PROPERTIES
  • ACCEPT_CASE_INSENSITIVE_ENUMS
  • ACCEPT_CASE_INSENSITIVE_VALUES
  • USE_WRAPPER_NAME_AS_PROPERTY_NAME
  • USE_STD_BEAN_NAMING
  • ALLOW_EXPLICIT_PROPERTY_RENAMING
  • ALLOW_COERCION_OF_SCALARS
  • IGNORE_DUPLICATE_MODULE_REGISTRATIONS
  • IGNORE_MERGE_FOR_UNMERGEABLE
  • BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES

3.7 serialization序列化特性开关配置

serialization属性同mapper类似,也是一个Map类型的属性

/**
* Jackson on/off features that affect the way Java objects are serialized.
*/
private final Map<SerializationFeature, Boolean> serialization = new EnumMap<>(SerializationFeature.class);

3.8 deserialization反序列化开关配置

deserialization反序列化配置

/**
* Jackson on/off features that affect the way Java objects are deserialized.*/
private final Map<DeserializationFeature, Boolean> deserialization = new EnumMap<>(DeserializationFeature.class);

3.9 parser配置

3.10 generator配置

3.11 defaultPropertyInclusion序列化包含的属性配置

该属性是一个枚举配置,主要包含:

  • ALWAYS:顾名思义,始终包含,和属性的值无关
  • NON_NULL:值非空的属性才会包含属性
  • NON_ABSENT:值非空的属性,或者Optional类型的属性非空
  • NON_EMPTY: 空值的属性不包含
  • NON_DEFAULT:不使用jackson的默认规则对该字段进行序列化,详见示例
  • CUSTOM:自定义规则
  • USE_DEFAULTS:配置使用该规则的属性字段,将会优先使用class上的注解规则,否则会使用全局的序列化规则,详见示例

CUSTOM自定义规则是需要开发者在属性字段上使用@JsonInclude注解,并且指定valueFilter属性,该属性需要传递一个Class,示例如下:

//User.java
//指定value级别是CUSTOM
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = StringFilter.class)
private String name;

StringFilter则是判断非空的依据,该依据由开发者自己定义,返回true将会被排除,false则不会排除,示例如下:

//自定义非空判断规则
public class StringFilter {@Overridepublic boolean equals(Object other) {if (other == null) {// Filter null's.return true;}// Filter "custom_string".return "custom_string".equals(other);}
}

4.Spring Boot针对Jackson的约定配置做的事情

在前面的文章中,我们已经详细的了解了Jackson在Spring Boot框架中的各个配置项,那么Spring Boot针对Jackson框架在约定配置时会做哪些事情呢?

在Spring Boot的spring-boot-autoconfigure-x.x.jar包中,我们可以看到Spring Boot框架针对jackson的处理源码,如下图:

image-20210320122535467

主要包含三个类:

  • JacksonProperties:Spring Boot框架提供jackson的配置属性类,即开发者在application.yml配置文件中的配置项属性
  • JacksonAutoConfiguration:Jackson的默认注入配置类
  • Jackson2ObjectMapperBuilderCustomizer:自定义用于注入jackson的配置辅助接口

核心类是JacksonAutoConfiguration.java,该类是Spring Boot框架将Jackson相关实体Bean注入Spring容器的关键配置类。其主要作用:

  • 注入Jackson的ObjectMapper实体Bean到Spring容器中
  • 注入ParameterNamesModule实体Bean到Spring容器中
  • 注入Jackson2ObjectMapperBuilder实体Bean
  • 注入JsonComponentModule实体Bean
  • 注入StandardJackson2ObjectMapperBuilderCustomizer实体Bean,该类是上面Jackson2ObjectMapperBuilderCustomizer的实现类,主要用于接收JacksonProperties属性,将Jackson的外部配置属性接收,然后最终执行customize方法,构建ObjectMapper所需要的Jackson2ObjectMapperBuilder属性,最终为ObjectMapper属性赋值准备

源码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {private static final Map<?, Boolean> FEATURE_DEFAULTS;static {Map<Object, Boolean> featureDefaults = new HashMap<>();featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);}@Beanpublic JsonComponentModule jsonComponentModule() {return new JsonComponentModule();}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)static class JacksonObjectMapperConfiguration {@Bean@Primary@ConditionalOnMissingBeanObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {return builder.createXmlMapper(false).build();}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(ParameterNamesModule.class)static class ParameterNamesModuleConfiguration {@Bean@ConditionalOnMissingBeanParameterNamesModule parameterNamesModule() {return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)static class JacksonObjectMapperBuilderConfiguration {@Bean@Scope("prototype")@ConditionalOnMissingBeanJackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,List<Jackson2ObjectMapperBuilderCustomizer> customizers) {Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();builder.applicationContext(applicationContext);customize(builder, customizers);return builder;}private void customize(Jackson2ObjectMapperBuilder builder,List<Jackson2ObjectMapperBuilderCustomizer> customizers) {for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {customizer.customize(builder);}}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)@EnableConfigurationProperties(JacksonProperties.class)static class Jackson2ObjectMapperBuilderCustomizerConfiguration {@BeanStandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(ApplicationContext applicationContext, JacksonProperties jacksonProperties) {return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);}static final class StandardJackson2ObjectMapperBuilderCustomizerimplements Jackson2ObjectMapperBuilderCustomizer, Ordered {private final ApplicationContext applicationContext;private final JacksonProperties jacksonProperties;StandardJackson2ObjectMapperBuilderCustomizer(ApplicationContext applicationContext,JacksonProperties jacksonProperties) {this.applicationContext = applicationContext;this.jacksonProperties = jacksonProperties;}@Overridepublic int getOrder() {return 0;}@Overridepublic void customize(Jackson2ObjectMapperBuilder builder) {if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());}if (this.jacksonProperties.getTimeZone() != null) {builder.timeZone(this.jacksonProperties.getTimeZone());}configureFeatures(builder, FEATURE_DEFAULTS);configureVisibility(builder, this.jacksonProperties.getVisibility());configureFeatures(builder, this.jacksonProperties.getDeserialization());configureFeatures(builder, this.jacksonProperties.getSerialization());configureFeatures(builder, this.jacksonProperties.getMapper());configureFeatures(builder, this.jacksonProperties.getParser());configureFeatures(builder, this.jacksonProperties.getGenerator());configureDateFormat(builder);configurePropertyNamingStrategy(builder);configureModules(builder);configureLocale(builder);}//more configure methods...}
}

总结:通过一系列的方法,最终构造一个生产级别可用的ObjectMapper对象,供在Spring Boot框架中对Java对象实现序列化与反序列化操作。

5.Jackson常见注解使用示例

备注:本小结内容来源https://www.baeldung.com/jackson-annotations,如果工作中对于jackson的注解使用较少的情况下,可以看看该篇文章,是一个非常好的补充。

5.1 序列化

5.1.1 @JsonAnyGetter

@JsonAnyGetter注解运行可以灵活的使用Map类型的作为属性字段

实体类如下:

public class ExtendableBean {public String name;private Map<String, String> properties;@JsonAnyGetterpublic Map<String, String> getProperties() {return properties;}public ExtendableBean(String name) {this.name = name;this.properties=new HashMap<String, String>();}public void add(String key,String value){this.properties.put(key,value);}
}

通过序列化该实体Bean,我们将会得到Map属性中的所有Key作为属性值,测试序列化代码如下:

@Test
public void whenSerializingUsingJsonAnyGetter_thenCorrect()throws JsonProcessingException {ExtendableBean bean = new ExtendableBean("My bean");bean.add("attr1", "val1");bean.add("attr2", "val2");String result = new ObjectMapper().writeValueAsString(bean);assertThat(result, containsString("attr1"));assertThat(result, containsString("val1"));
}

最终输出结果如下:

{"name":"My bean","attr2":"val2","attr1":"val1"
}

如果不使用@JsonAnyGetter注解,那么最终序列化结果将会在properties属性下面,结果如下:

{"name": "My bean","properties": {"attr2": "val2","attr1": "val1"}
}

5.1.2 @JsonGetter

@JsonGetter注解是一个替代@JsonProperty的注解,可以将一个方法标注为getter方法

例如下面的示例中,我们通过注解@JsonGetter将方法getTheName()作为属性namegetter方法

public class MyBean {public int id;private String name;@JsonGetter("name")public String getTheName() {return name;}
}

5.1.3 @JsonPropertyOrder

可以通过使用@JsonPropertyOrder注解来指定属性的序列化顺序

实体bean定义如下:

@JsonPropertyOrder({ "name", "id" })
public class MyBean {public int id;public String name;
}

最终序列化结果为:

{"name":"My bean","id":1
}

也可以通过@JsonPropertyOrder(alphabetic=true)来指定按照字母排序,那么响应结果将是:

{"id":1,"name":"My bean"
}

5.1.4 @JsonRawValue

@JsonRawValue注解可以指定字符串属性类为json,如下代码:

public class RawBean {public String name;@JsonRawValuepublic String json;
}

创建RawBean的示例,给属性json赋值,代码如下:

 RawBean bean = new RawBean("My bean", "{\"attr\":false}");String result = new ObjectMapper().writeValueAsString(bean);

最终序列化结果如下:

{"name":"My bean","json":{"attr":false}
}

5.1.5 @JsonValue

@JsonValue注解主要用于序列化整个实例对象的单个方法,例如,在一个枚举类中,@JsonValue注解进行标注,代码如下:

public enum  TypeEnumWithValue {TYPE1(1, "Type A"), TYPE2(2, "Type 2");private Integer id;private String name;TypeEnumWithValue(Integer id, String name) {this.id = id;this.name = name;}@JsonValuepublic String getName() {return name;}
}

测试代码如下:

String enumAsString = new ObjectMapper().writeValueAsString(TypeEnumWithValue.TYPE1);
System.out.println(enumAsString);

最终通过序列化代码得到的结果将是:

"Type A"

5.1.6 @JsonRootName

@JsonRootName注解旨在给当前序列化的实体对象加一层包裹对象。

举例如下:

//RootUser.java
public class RootUser {private String name;private String title;public RootUser(String name, String title) {this.name = name;this.title = title;}//getter and setters  
}

在上面的实体类中,正常情况下,如果要序列号RootUser对象,其结果格式为:

{"name": "name1","title": "title1"
}

RootUser加上@JsonRootName注解后,该类改动如下:

//RootUser.java
@JsonRootName(value = "root")
public class RootUser {private String name;private String title;public RootUser(String name, String title) {this.name = name;this.title = title;}//getter and setters  
}

启用ObjectMapper对象的WRAP_ROOT_VALUE特性,测试代码如下:

ObjectMapper objectMapper=new ObjectMapper();
objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
String result=objectMapper.writeValueAsString(new RootUser("name1","title1"));

最终序列化JSON结果如下:

{"root": {"name": "name1","title": "title1"}
}

5.1.7 @JsonSerialize

@JsonSerialize注解允许开发者自定义序列化实现,来看代码实现

public class EventWithSerializer {public String name;@JsonSerialize(using = CustomDateSerializer.class)public Date eventDate;public Date publishDate;//getter and setter...
}

在上面的代码中,针对eventDate字段,我们通过使用@JsonSerialize注解,自定义了一个序列化实现类CustomDateSerializer,该类实现如下:

//CustomDateSerializer.java
public class CustomDateSerializer extends StdSerializer<Date> {private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");public CustomDateSerializer() { this(null); } public CustomDateSerializer(Class<Date> t) {super(t); }@Overridepublic void serialize(Date value, JsonGenerator gen, SerializerProvider arg2) throws IOException, JsonProcessingException {gen.writeString(formatter.format(value));}
}

最终序列化的结果格式如下:

{"name": "名称","eventDate": "24-03-2021 06:14:32","publishDate": 1616580872574
}

从结果我们可以得知,针对某个特定的字段序列化的方式,我们可以完全自定义,非常的方便。

5.2 反序列化

5.2.1 @JsonCreator

@JsonCreator配合@JsonProperty注解能到达在反序列化实体对象时,指定不变更属性名称的效果

例如有如下JSON:

{"id":1,"theName":"My bean"
}

在实体类中,我们没有属性名称是theName,但我们想把theName属性反序列化时赋值给name,此时实体类对象结构如下:

public class BeanWithCreator {public int id;public String name;@JsonCreatorpublic BeanWithCreator(@JsonProperty("id") int id, @JsonProperty("theName") String name) {this.id = id;this.name = name;}
}

BeanWithCreator的构造函数中添加@JsonCreator注解,并且配合@JsonProperty注解进行属性指向,最终反序列化代码如下:

@Test
public void whenDeserializingUsingJsonCreator_thenCorrect()throws IOException {String json = "{\"id\":1,\"theName\":\"My bean\"}";BeanWithCreator bean = new ObjectMapper().readerFor(BeanWithCreator.class).readValue(json);assertEquals("My bean", bean.name);
}

5.2.2 @JacksonInject

@JacksonInject注解可以指定反序列化对象时,属性值不从来源JSON获取,而从injection中获取

实体类如下:

public class BeanWithInject {@JacksonInjectpublic int id;public String name;
}

反序列化代码

@Test
public void whenDeserializingUsingJsonInject_thenCorrect()throws IOException {String json = "{\"name\":\"My bean\"}";InjectableValues inject = new InjectableValues.Std().addValue(int.class, 1);BeanWithInject bean = new ObjectMapper().reader(inject).forType(BeanWithInject.class).readValue(json);assertEquals("My bean", bean.name);assertEquals(1, bean.id);
}

5.2.3 @JsonAnySetter

@JsonAnySetter@JsonAnyGetter注解意思一致,只不过是针对序列化与反序列化而言,@JsonAnySetter注解可以将来源JSON最终转化为Map类型的属性结构

实体代码如下:

public class ExtendableBean {public String name;private Map<String, String> properties;@JsonAnySetterpublic void add(String key, String value) {properties.put(key, value);}
}

JSON源如下:

{"name":"My bean","attr2":"val2","attr1":"val1"
}

通过@JsonAnySetter的注解标注,最终attr1attr2的值将会添加到properties的Map对象中

示例代码如下:

@Test
public void whenDeserializingUsingJsonAnySetter_thenCorrect()throws IOException {String json= "{\"name\":\"My bean\",\"attr2\":\"val2\",\"attr1\":\"val1\"}";ExtendableBean bean = new ObjectMapper().readerFor(ExtendableBean.class).readValue(json);assertEquals("My bean", bean.name);assertEquals("val2", bean.getProperties().get("attr2"));
}

5.2.4 @JsonSetter

@JsonSetter注解是@JsonProperty的替代注解,用于标注该方法为setter方法

当我们需要读取一些JSON数据时,但是目标实体类与该数据不完全匹配是,该注解是非常有用的。

示例代码如下:

public class MyBean {public int id;private String name;@JsonSetter("name")public void setTheName(String name) {this.name = name;}
}

通过指定setTheName作为属性namesetter方法,反序列化时可以达到最终效果

示例如下:

@Test
public void whenDeserializingUsingJsonSetter_thenCorrect()throws IOException {String json = "{\"id\":1,\"name\":\"My bean\"}";MyBean bean = new ObjectMapper().readerFor(MyBean.class).readValue(json);assertEquals("My bean", bean.getTheName());
}

5.2.5 @JsonDeserialize

@JsonDeserialize注解和序列化注解@JsonSerialize的效果是一致的,作用与反序列化时,针对特定的字段,存在差异化的发序列化效果

public class EventWithSerializer {public String name;@JsonDeserialize(using = CustomDateDeserializer.class)public Date eventDate;
}

CustomDateDeserializer代码如下:

public class CustomDateDeserializerextends StdDeserializer<Date> {private static SimpleDateFormat formatter= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");public CustomDateDeserializer() { this(null); } public CustomDateDeserializer(Class<?> vc) { super(vc); }@Overridepublic Date deserialize(JsonParser jsonparser, DeserializationContext context) throws IOException {String date = jsonparser.getText();try {return formatter.parse(date);} catch (ParseException e) {throw new RuntimeException(e);}}
}

最终,反序列化JSON,时,得到eventDate字段,测试代码如下:

@Test
public void whenDeserializingUsingJsonDeserialize_thenCorrect()throws IOException {String json= "{"name":"party","eventDate":"20-12-2014 02:30:00"}";SimpleDateFormat df= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");EventWithSerializer event = new ObjectMapper().readerFor(EventWithSerializer.class).readValue(json);assertEquals("20-12-2014 02:30:00", df.format(event.eventDate));
}

5.2.6 @JsonAlias

@JsonAlias注解作用于可以指定一个别名与JSON数据中的字段进行对于,最终反序列化时,能将该值最终反序列化时赋值给对象

实体如下:

public class AliasBean {@JsonAlias({ "fName", "f_name" })private String firstName;   private String lastName;
}

上面的代码中,firstName字段通过@JsonAlias注解指定了两个别名字段,意思是反序列化时可以从JSON中读取fName或者f_name的值赋值到firstName

测试代码如下:

@Test
public void whenDeserializingUsingJsonAlias_thenCorrect() throws IOException {String json = "{\"fName\": \"John\", \"lastName\": \"Green\"}";AliasBean aliasBean = new ObjectMapper().readerFor(AliasBean.class).readValue(json);assertEquals("John", aliasBean.getFirstName());
}

5.3 属性注解

5.3.1 @JsonIgnoreProperties

使用@JsonIgnoreProperties注解作用于class级别中可以达到在序列化时忽略一个或多个字段的效果

实体代码如下:

@JsonIgnoreProperties({ "id" })
public class BeanWithIgnore {public int id;public String name;
}

最终在序列化BeanWithIgnore实体对象时,字段id将会被忽略

5.3.2 @JsonIgnore

@JsonIgnore注解作用与属性级别中,在序列化时可以忽略该字段

实体代码如下:

public class BeanWithIgnore {@JsonIgnorepublic int id;public String name;
}

最终在序列化BeanWithIgnore实体对象时,字段id将会被忽略

5.3.3 @JsonIgnoreType

@JsonIgnoreType指定忽略类型属性

public class User {public int id;public Name name;@JsonIgnoreTypepublic static class Name {public String firstName;public String lastName;}
}

在上面的示例中,类型Name将会被忽略

5.3.4 @JsonInclude

使用@JsonInclude注解可以排除属性值中包含empty/null/default的属性

@JsonInclude(Include.NON_NULL)
public class MyBean {public int id;public String name;
}

MyBean中使用了Include.NON_NULL则代表该实体对象序列化时不会包含空值

5.3.5 @JsonAutoDetect

@JsonAutoDetect可以覆盖实体对象属性中的默认可见级别,比如私有属性可见与不可见

实体对象如下:

public class PrivateBean {private int id;private String name;public PrivateBean(int id, String name) {this.id = id;this.name = name;}
}

PrivateBean中,没有给属性字段idname设置公共的getter方法,此时,如果我们如果直接对该实体对象进行序列化时,jackson会提示错误

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.xiaoymin.boot.action.jackson.model.PrivateBean and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

我们修改PrivateBean中的代码,增加@JsonAutoDetect注解,代码如下:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class PrivateBean {private int id;private String name;public PrivateBean(int id, String name) {this.id = id;this.name = name;}
}

此时,在序列化该实体对象,将会得到响应结果

PrivateBean bean = new PrivateBean(1, "My bean");
String result = new ObjectMapper().writeValueAsString(bean);
System.out.println(result);

5.4 常规注解

5.4.1 @JsonProperty

我们可以添加@JsonProperty批注以在JSON中指示属性名称。

当实体对象中没有标准的getter/setter方法时,我们可以使用该注解进行指定属性名称已方便jackson框架进行序列化/反序列化

public class MyBean {public int id;private String name;@JsonProperty("name")public void setTheName(String name) {this.name = name;}@JsonProperty("name")public String getTheName() {return name;}
}

5.4.2 @JsonFormat

针对日期字段可以通过使用@JsonFormat注解进行格式化输出

public class EventWithFormat {public String name;@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "dd-MM-yyyy hh:mm:ss")public Date eventDate;
}

5.4.3 @JsonUnwrapped

@JsonUnwrapped注解可以指定jackson框架在序列化/反序列化时是否需要对该字段进行wrapped操作

示例代码:

public class UnwrappedUser {public int id;@JsonUnwrappedpublic Name name;//getter and setter...public static class Name {public String firstName;public String lastName;//getter and setter}
}

通过注解@JsonUnwrapped标注name属性,最终序列化该对象时,会和正常情况下有所区别

UnwrappedUser.Name name = new UnwrappedUser.Name("John", "Doe");
UnwrappedUser user = new UnwrappedUser(1, name);String result = new ObjectMapper().writeValueAsString(user);

我们得到的结果如下:

{"id": 1,"firstName": "John","lastName": "Doe"
}

5.4.4 @JsonView

通过View的方式来指定序列化/反序列化时是否包含属性

示例代码如下:

View定义

public class Views {public static class Public {}public static class Internal extends Public {}
}

实体代码:

public class Item {@JsonView(Views.Public.class)public int id;@JsonView(Views.Public.class)public String itemName;@JsonView(Views.Internal.class)public String ownerName;//getter and setter..}

最终序列化代码示例:

Item item = new Item(2, "book", "John");String result = new ObjectMapper().writerWithView(Views.Public.class).writeValueAsString(item);
System.out.println(result);

最终序列化结果输出:

{"id":2,"itemName":"book"}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/555851.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java hdfs连接池_Java使用连接池管理Hdfs连接

记录一下Java API 连接hadoop操作hdfs的实现流程(使用连接池管理)。以前做过这方面的开发&#xff0c;本来以为不会有什么问题&#xff0c;但是做的还是坑坑巴巴&#xff0c;内心有些懊恼&#xff0c;记录下这烦人的过程&#xff0c;警示自己切莫眼高手低&#xff01;一&#x…

为了拿捏 Redis 数据结构,我画了 40 张图(完整版)

Redis 为什么那么快&#xff1f; 除了它是内存数据库&#xff0c;使得所有的操作都在内存上进行之外&#xff0c;还有一个重要因素&#xff0c;它实现的数据结构&#xff0c;使得我们对数据进行增删查改操作时&#xff0c;Redis 能高效的处理。 因此&#xff0c;这次我们就来…

fastjson为什么默认是无序的

在做项目的时候&#xff0c;无意间发现添加到json中的元素是无存放顺序的。严格来说&#xff0c;json默认是有存放顺序的&#xff0c;不过是采用HashCode值来排序。下面来看一段源码 上图展示了创建json对象的一个过程&#xff0c;可以清晰的看出&#xff0c;无论用户调用哪个…

Java之HashMap.values()转List时的错误和正确操作

因为项目中需要获取到Map的值的集合&#xff0c;所以调用了HashMap.values()方法转成List&#xff0c;当时是使用了以下代码。&#xff08;逻辑上这样想应该没问题&#xff0c;但生活总是会是不是给你一点小“”惊喜“”&#xff09; List<AreaItemOpt> areaItemOpts (…

php检测表大小,查询mysql数据库、表的大小

一、关于mysql表数据大小mysql存储数据文件一般使用表空间存储 &#xff1b;当mysql使用innodb存储引擎的时候&#xff0c;mysql使用表存储数据分为共享表空间和独享表空间两种方式 。共享表空间&#xff1a;Innodb的所有数据保存在一个单独的表空间里面&#xff0c;而这个表空…

springboot使用redis(StringRedisTemplate的常用方法)

1. 先了解RedisTemplate和StringRedisTemplate之间的关系&#xff1a; RedisTemplate是Spring对于Redis的封装&#xff0c;而StringRedisTemplate继承RedisTemplate。两者的数据是不共通的&#xff1b;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据&…

Arrays.asList()和Collections.singletonList()比较

Collections.singletonList(something)是不可变的&#xff0c; 对Collections.singletonList(something)返回的列表所做的任何更改将导致UnsupportedOperationException。 Arrays.asList(something)允许Arrays.asList(something) 更改 。 此外&#xff0c;由Collections.sin…

php 邮件类库,[3.3]-扩展类库:基于PHPMailer的邮件发送 | PhalApi(π框架) - PHP轻量级开源接口框架 - 接口,从简单开始!...

3.3.1 扩展类库&#xff1a;基于PHPMailer的邮件发送此扩展可用于发送邮件。3.3.2 安装和配置从 PhalApi-Library 扩展库中下载获取 PHPMailer 包&#xff0c;如使用&#xff1a;$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git然后把 PHPMailer 目录复制到 …

spring boot报FileSizeLimitExceededException异常的解决方法

开发spring boot程序时&#xff0c;遇到了如下错误&#xff1a; The field file exceeds its maximum permitted size of 1048576 bytes. 原因&#xff1a; Spring Boot工程嵌入的tomcat限制了请求的文件大小&#xff0c;官方文档中这样描述&#xff1a; Spring Boot embraces…

前后端分离中的权限管理思路

在传统的前后端不分的开发中&#xff0c;权限管理主要通过过滤器或者拦截器来进行&#xff08;权限管理框架本身也是通过过滤器来实现功能&#xff09;&#xff0c;如果用户不具备某一个角色或者某一个权限&#xff0c;则无法访问某一个页面。 但是在前后端分离中&#xff0c;…

前、后端分离权限控制设计与实现

简述 近几年随着react、angular、vue等前端框架兴起&#xff0c;前后端分离的架构迅速流行。但同时权限控制也带来了问题。 网上很多前、后端分离权限仅仅都仅仅在描述前端权限控制、且是较简单、固定的角色场景&#xff0c;满足不了我们用户、角色都是动态的场景。且仅仅前端…

前后端分离必备的接口规范,十分接地气

1. 前言 随着互联网的高速发展&#xff0c;前端页面的展示、交互体验越来越灵活、炫丽&#xff0c;响应体验也要求越来越高&#xff0c;后端服务的高并发、高可用、高性能、高扩展等特性的要求也愈加苛刻&#xff0c;从而导致前后端研发各自专注于自己擅长的领域深耕细作。 然…

ubuntu php设置,关于ubuntu php环境设置详解-PHP问题

ubuntu php设置办法&#xff1a;起首更新源列表&#xff1b;而后关上“终端窗口”&#xff0c;输出饬令“sudo apt-get install php5”来装置php&#xff1b;接着装置设置装备摆设好apache环境&#xff0c;并装置php5-gd模块&#xff1b;最初创立“info.php”文件便可。Ubuntu …

MyBatis-Plus——增删查改

开发环境 IDEA JDK&#xff1a;1.8 Spring Boot:2.6.2 Maven:3.3.9 MySQL:8.0.23 数据库准备 CREATE DATABASE mybatis_plus_db;DROP TABLE IF EXISTS person; CREATE TABLE person(id BIGINT(20) NOT NULL COMMENT 主键ID, name VARCHAR(30) NULL DEFAULT NULL COMMENT 姓…

让程序员最爽的ThreadLocal使用姿势

一、常见场景 ​ 1、ThreadLocal作为线程上下文副本&#xff0c;那么一种最常见的使用方式就是用来方法隐式传参&#xff0c;通过提供的set()和get()两个public方法来实现在不同的方法中的参数传递。对于编程规范来说&#xff0c;方法定义的时候是对参数个数是有限制的&#x…

一场事故告诉你zookeeper和nacos谁更适合做注册中心

前言 ​ 在分布式系统中&#xff0c;注册中心充当着重要角色&#xff0c;是服务发现、客户端负载均衡中不可缺少的一员。注册中心除了能够实现基本的功能外&#xff0c;他的稳定性、可用性和健壮性对整个分布式系统的流畅运行影响重大。dubbo作为国内一款主流的分布式系统&…

Mysql执行计划含义,mysql执行计划介绍

烂sql不仅直接影响sql的响应时间&#xff0c;更影响db的性能&#xff0c;导致其它正常的sql响应时间变长。如何写好sql&#xff0c;学会看执行计划至关重要。下面我简单讲讲mysql的执行计划&#xff0c;只列出了一些常见的情况&#xff0c;希望对大家有所帮助。测试表结构&…

寄生虫php版,-PHP版SEO最新教材版排名DeDeCms寄生虫V90繁殖

今天视频教程演示说明下PHP寄生虫服务端的使用。主要在很多搭建的过程中会出错&#xff0c;今天主要就讲解下寄生虫配置常见的问题。那么同样也可以看下我们之前的通用版寄生虫使用教程[通用版教程在文件夹中有]&#xff0c;本教程是针对PHP版本的寄生虫服务端进行配置演示。继…

MySQL中 JSON 数据类型应用

前言 今天接触到mysql中json数据类型&#xff0c;之前不知道有这个类型&#xff0c;今天学习一下。 JSON我相信大家都已经很熟悉了&#xff0c;但在 MySQL中&#xff0c;直至 5.7 版本中&#xff0c;才正式引入 JSON数据类型。在次之前&#xff0c;我们通常使varchar或text数…

MySql中json类型的使用___mybatis存取mysql中的json

MySql中json类型的使用 MySQL从5.7.8起开始支持JSON字段&#xff0c;这极大的丰富了MySQL的数据类型。也方便了广大开发人员。但MySQL并没有提供对JSON对象中的字段进行索引的功能&#xff0c;至少没有直接对其字段进行索引的方法。本文将介绍利用MySQL 5.7中的虚拟字段的功能…