背景

  在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",内容如下

java hutool 获取json所有key java获取json的key_spring

  • 配置文件解析类
      用于解析所有配置文件,其中第一层结构是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)

java hutool 获取json所有key java获取json的key_spring_02

测试

测试代码

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());
    }

测试结果

java hutool 获取json所有key java获取json的key_配置文件_03