背景
在json字符串和对象互转功能中,遇到了这样的情况,知道数据接口的要素是什么。但是不知道具体的key是什么。举个例子,知道接口中有“用户名 / 密码”,”两个要素,但是不知道对应key是“usename/password”,还是“uname/passwd”,还是“user_name/passwrod”。
为了开发进度,就设想先把类和类中的属性创建出来,然后把属性对应的key配置到文件中,在json字符串和对象互转时,从配置文件获取类属性对应的json key。等拿到接口文档是,把配置文件做好,不用改代码就可以正常工作了。json工具类使用的是jackson,在导入springboot依赖时,会一起导入,不用单独导入这个依赖。
开发过程中问题
最初准备使用@JsonProperty(“new-json-key”)注解,这个注解用在字段上,意思是用“new-json-key”
的值替换字段名。但有个问题,这个注解的值是个常量,不能随着配置动态变化。于是尝试修改成这样@JsonProperty(“${new-json-key}”),这在springboot的注解中很常见。下面就要来“但是"了.@JsonProperty不是springboot管理的注解,压根就不会替换。这条路就失败了。。。
接着尝试使用自定义序列化/反序列化类 JsonSerializer/JsonDeserializer,经过测试,发现这两个类是针对“值“做自定义出入输出的。于当初的设想不一致
最后找到了PropertyNamingStrategy类(属性命名策略),这个类是针对key做个性化处理的。下面就介绍一下实现过程。
实现过程
- 配置文件设计
配置文件以Bean做单位,每一个Bean配置一个文件。其中key是类的属性,value是实际的jsonkey
例如,假定 "用户名"要素 key 为“user_name”,类中属性 定义为“userName”
那么,先创建UserInfo类,类中有属性,userName
package com.ylink.cfcust.core.config.jackson;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
/**
* @create 2024/1/23 16:48
*/
@Data //lombok注解
@JsonNaming(MyPropertyNamingStrategy.class)//自定义属性命名策略
public class UserInfo {
private String userName;
}
对应配置文件名为"UserInfo.properties",内容如下
- 配置文件解析类
用于解析所有配置文件,其中第一层结构是map,key为文件名(不包括后缀),第一层map的value是以配置文件的key-value为内容的map
package com.ylink.cfcust.core.config.jackson;
import com.ylink.cfcust.core.utils.PropertiesUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileFilter;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 加载配置文件
*/
@Component
public class JacksonKeyConfig {
//配置文件的目录,可以配置在Application.yml文件里
@Value("${jackson.json-keys.path:./config/jsonkeys}")
private String configFilePath;
private Map<String, Map<String, String>> jsonkeyMap = new HashMap<>();
@PostConstruct//spring初始化实例后,执行本方法,自动加载所以配置文件
public void init (){
File fileDir = new File(configFilePath);
if(!fileDir.exists()){
return;
}
File[] props = fileDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if (pathname.isDirectory()) {
//过滤掉目录
return false;
}
//过滤非 PROPERTIES 文件
return pathname.getName().toUpperCase().endsWith("PROPERTIES");
}
});
if(props == null || props.length == 0){
return;
}
for (File prop: props){
Map<String, String> configMap = parsePropFile2Map(prop);
String key = prop.getName().substring(0, prop.getName().lastIndexOf("."));
jsonkeyMap.put(key, configMap);
}
}
private Map<String, String> parsePropFile2Map(File file) {
Properties prop = new Properties();
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
prop.load(fis);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
}
}
}
if(prop != null){
Map<String, String> valueMap = new HashMap<String, String>();
Set<Object> keys = prop.keySet();
Iterator<?> it = keys.iterator();
while (it.hasNext()) {
String key = String.valueOf(it.next());
valueMap.put(key, prop.getProperty(key));
}
return valueMap;
}
return null;
}
public String getKeyMapping(String className, String key){
if(!this.jsonkeyMap.containsKey(className)){
return null;
}
return this.jsonkeyMap.get(className).get(key);
}
}
- jackson ObjectMapper 初始化类
package com.ylink.cfcust.core.config.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
*/
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper;
}
}
- jackson 工具类 JacksonUtil
这个类注入了ObjectMapper 和JacksonKeyConfig 的实例。提供了Json字符串和对象互转的方法
package com.ylink.cfcust.core.config.jackson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
*
* 工具类
*/
@Component
public class JacksonUtil {
private static ObjectMapper objectMapper;
private static JacksonKeyConfig jacksonKeyConfig;
public static String bean2Str (Object bean) throws JsonProcessingException {
if(bean == null){
return null;
}
try {
String jsonStr = objectMapper.writeValueAsString(bean);
return jsonStr;
} catch (JsonProcessingException e) {
throw e;
}
}
public static String bean2StrForamt (Object bean) throws JsonProcessingException {
if(bean == null){
return null;
}
try {
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(bean);
return jsonStr;
} catch (JsonProcessingException e) {
throw e;
}
}
public static <T> T jsonStr2Bean (String jsonStr, Class<T> cls) throws IOException {
if(jsonStr == null || "".equalsIgnoreCase(jsonStr)){
return null;
}
if(cls == null){
return null;
}
try {
T t = objectMapper.readValue(jsonStr, cls);
return t;
} catch (IOException e) {
throw e;
}
}
public static String getKeyMapping( String className, String key){
if(className == null || key == null){
return null;
}
return JacksonUtil.jacksonKeyConfig.getKeyMapping(className, key);
}
public static String getKeyMapping(String value) {
if(value == null){
return null;
}
String[] strArray = value.split("[.]");
if(strArray == null || strArray.length < 2){
return null;
}
return getKeyMapping(strArray[0], strArray[1]);
}
@Autowired
public void setObjectMapper(ObjectMapper objectMapper) {
JacksonUtil.objectMapper = objectMapper;
}
@Autowired
public void setJacksonKeyConfig(JacksonKeyConfig jacksonKeyConfig) {
JacksonUtil.jacksonKeyConfig = jacksonKeyConfig;
}
}
- 自定义属性命名策略类
package com.ylink.cfcust.core.config.jackson;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import java.lang.reflect.Field;
import java.util.*;
/**
* @create 2024/1/24 18:03
*/
public class MyPropertyNamingStrategy extends PropertyNamingStrategy.SnakeCaseStrategy {
/**
* 用于序列化时 key 的重命名
* @param config
* @param method
* @param defaultName 类中的属性名
* @return
*/
@Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
//正在处理bean对象的Class
Class<?> clazz = method.getDeclaringClass();
//不带包名的类名
String className = clazz.getSimpleName();
String realKeyName;
//去配置中获取实际key
String tmpKeyNmae = JacksonUtil.getKeyMapping(className, defaultName);
if(tmpKeyNmae != null){
//配置存在,使用配置值
realKeyName = tmpKeyNmae;
}else {
//配置不存在,使用类中定义的原始属性名
realKeyName = defaultName;
}
return realKeyName;
}
/**
* 用于反序列化时 key 的重命名
* @param config
* @param method
* @param defaultName 类中的属性名
* @return
*/
@Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
//正在处理bean对象的Class
Class<?> clazz = method.getDeclaringClass();
//不带包名的类名,和配置文件不带后缀的we年明一致
String className = clazz.getSimpleName();
String realKeyName;
//去配置中获取实际key
String tmpKeyNmae = JacksonUtil.getKeyMapping(className, defaultName);
if(tmpKeyNmae != null){
//配置存在,使用配置值
realKeyName = tmpKeyNmae;
}else {
//配置不存在,使用类中定义的原始属性名
realKeyName = defaultName;
}
return realKeyName;
}
}
属性命名策略类的使用,是在类名加注解@JsonNaming(MyPropertyNamingStrategy.class)
测试
测试代码
public void test1() throws IOException {
UserInfo userInfo = new UserInfo();
userInfo.setUserName("test");
//bean转json字符串
String studentStr = objectMapper.writeValueAsString(userInfo);
System.out.println("bean 2 jsonStr ->"+studentStr);
String jsonStr = "{\"user_name\":\"test\"}";
//json字符串转对象
UserInfo testBean1 = JacksonUtil.jsonStr2Bean(jsonStr, UserInfo.class);
System.out.println("jsonStr["+jsonStr+"] 2 bean ->"+testBean1.toString());
}
测试结果