专业的编程技术博客社区

网站首页 > 博客文章 正文

SpringBoot中Jackson实现自定义序列化和反序列化控制5种方式总结

baijin 2025-01-16 17:48:28 博客文章 8 ℃ 0 评论

?

SpringBoot默认使用Jackson序反列化,但日常开发中经常需要自定义控制其过程, 比如: 控制字段是否序反列化,改变返回Json结构,顺序,格式等等。可以通过多种方式来实现,以下是一些常用的方式。

?

  • 一、自定义Serializer和Deserializer
  • 二、使用Mix-in Annotations
  • 三、使用注解
  • 四、使用ObjectMapper
  • 五、使用BeanSerializerModifier
  • 六、使用案例:枚举、字典数据的自动转化
    • 6.1 实现思路
    • 6.2 代码实现

一、自定义Serializer和Deserializer

当需要对特定的类或属性进行自定义的序列化和反序列化控制时,可编写自定义的序列化器(Serializer)和反序列化器(Deserializer)。通过实现 JsonSerializer 和 JsonDeserializer 接口,可以完全控制序列化和反序列化过程中的行为,包括如何读取属性、生成 JSON 或者解析 JSON 等。

使用自定义序列化器和反序列化器来控制日期格式的序列化和反序列化:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;


import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class CustomDateSerializationExample {


    public static class DateSerializer extends JsonSerializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            String formattedDate = DATE_FORMAT.format(value);
            gen.writeString(formattedDate);
        }
    }


    public static class DateDeserializer extends JsonDeserializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String dateString = p.getText();
            try {
                return DATE_FORMAT.parse(dateString);
            } catch (Exception e) {
                throw new IOException("Failed to parse date: " + dateString, e);
            }
        }
    }


    public static class Person {
        private String name;
        private Date birthDate;
        // 省略构造和getter/setter方法
    }


    public static void main(String[] args) throws Exception {
        SimpleModule module = new SimpleModule();
        module.addSerializer(Date.class, new DateSerializer());
        module.addDeserializer(Date.class, new DateDeserializer());


        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(module);


        // 序列化
        Person person = new Person("John Doe", new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"));
        String json = objectMapper.writeValueAsString(person);
        System.out.println(json);  
        // 输出:{"name":"John Doe","birthDate":"2000-01-01"}


        // 反序列化
        String jsonInput = "{\"name\":\"Jane Smith\",\"birthDate\":\"1990-12-31\"}";
        Person deserializedPerson = objectMapper.readValue(jsonInput, Person.class);
        System.out.println(deserializedPerson.getBirthDate());  
        // 输出:Sat Dec 31 00:00:00 GMT 1990
    }
}

二、使用Mix-in Annotations

Mix-in Annotations 允许在不修改原始类的情况下,为其添加自定义的序列化和反序列化逻辑。可以创建一个独立的 Mix-in 类,并在该类中为原始类添加自定义的注解,然后将 Mix-in 类与原始类关联起来。

用 Mix-in Annotations 来控制日期格式的序列化和反序列化:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;


import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class CustomDateSerializationExample {


    public static class Person {
        private String name;


        @JsonSerialize(using = DateSerializer.class)
        @JsonDeserialize(using = DateDeserializer.class)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthDate;


        // 省略构造函数和getter/setter方法
    }


    public static class DateSerializer extends JsonSerializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public void serialize(Date value, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider serializers) throws IOException {
            String formattedDate = DATE_FORMAT.format(value);
            gen.writeString(formattedDate);
        }
    }


    public static class DateDeserializer extends JsonDeserializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String dateString = p.getText();
            try {
                return DATE_FORMAT.parse(dateString);
            } catch (Exception e) {
                throw new IOException("Failed to parse date: " + dateString, e);
            }
        }
    }


    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();


        // 注册 Mix-in Annotations
        objectMapper.addMixIn(Person.class, PersonMixin.class);


        // 序列化
        Person person = new Person("John Doe", new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"));
        String json = objectMapper.writeValueAsString(person);
        System.out.println(json); 
        // 输出:{"name":"John Doe","birthDate":"2000-01-01"}


        // 反序列化
        String jsonInput = "{\"name\":\"Jane Smith\",\"birthDate\":\"1990-12-31\"}";
        Person deserializedPerson = objectMapper.readValue(jsonInput, Person.class);
        System.out.println(deserializedPerson.getBirthDate());  
        // 输出:Sat Dec 31 00:00:00 GMT 1990
    }


    // Mix-in Annotations类
    abstract class PersonMixin {
        @JsonSerialize(using = DateSerializer.class)
        @JsonDeserialize(using = DateDeserializer.class)
        @JsonFormat(pattern = "yyyy-MM-dd")
        abstract Date getBirthDate();
    }
}
  • 创建PersonMixin的Mix-in Annotations 类,包含Person类的birthDate属性相关的注解。通过将 PersonMixin 注册到 Person 类上,实现了对 birthDate 属性的自定义序列化和反序列化控制,而无需修改原始的 Person 类。在ObjectMapper对象中通过addMixIn方法将 PersonMixin 注册到 Person 类上。

使用 Mix-in Annotations,可以在不修改原始类的情况下,为其添加自定义的序列化和反序列化逻辑。这种方法非常灵活,适用于需要对多个类或属性进行自定义序列化和反序列化控制的场景。

三、使用注解

Jackson提供了多个注解,如 @JsonSerialize 和 @JsonDeserialize,可直接用于类或属性上,用来指定自定义的序列化器和反序列化器:

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonFormat;


import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class CustomDateSerializationExample {


    public static class Person {
        private String name;


        @JsonSerialize(using = DateSerializer.class)
        @JsonDeserialize(using = DateDeserializer.class)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthDate;


        // 省略构造函数和getter/setter方法
    }


    public static class DateSerializer extends JsonSerializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public void serialize(Date value, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider serializers) throws IOException {
            String formattedDate = DATE_FORMAT.format(value);
            gen.writeString(formattedDate);
        }
    }


    public static class DateDeserializer extends JsonDeserializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String dateString = p.getText();
            try {
                return DATE_FORMAT.parse(dateString);
            } catch (Exception e) {
                throw new IOException("Failed to parse date: " + dateString, e);
            }
        }
    }


    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();


        // 序列化
        Person person = new Person("John Doe", new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"));
        String json = objectMapper.writeValueAsString(person);
        System.out.println(json);  
        // 输出:{"name":"John Doe","birthDate":"2000-01-01"}


        // 反序列化
        String jsonInput = "{\"name\":\"Jane Smith\",\"birthDate\":\"1990-12-31\"}";
        Person deserializedPerson = objectMapper.readValue(jsonInput, Person.class);
        System.out.println(deserializedPerson.getBirthDate()); 
        // 输出:Sat Dec 31 00:00:00 GMT 1990
    }
}
  • 还使用了 @JsonFormat 注解来指定日期格式。这种方式非常简洁,适用于只需要对少量属性进行自定义序列化和反序列化控制的场景。

四、使用 ObjectMapper

通过配置ObjectMapper,注册自定义的模块或者处理器,如 SimpleModule 或者 HandlerInstantiator,来实现更高级的自定义序列化和反序列化控制。这些方法可以让你在全局范围内对序列化和反序列化行为进行定制。

使用 SimpleModule 和自定义的 HandlerInstantiator 来注册自定义的序列化器和反序列化器:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver;
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
import com.fasterxml.jackson.databind.module.SimpleSerializers;


import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class CustomDateSerializationExample {


    public static class Person {
        private String name;
        private Date birthDate;


        // 省略构造函数和getter/setter方法
    }


    public static class DateSerializer extends JsonSerializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public void serialize(Date value, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider serializers) throws IOException {
            String formattedDate = DATE_FORMAT.format(value);
            gen.writeString(formattedDate);
        }
    }


    public static class DateDeserializer extends JsonDeserializer<Date> {
        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");


        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String dateString = p.getText();
            try {
                return DATE_FORMAT.parse(dateString);
            } catch (ParseException e) {
                throw new IOException("Failed to parse date: " + dateString, e);
            }
        }
    }


    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();


        // 创建自定义模块
        SimpleModule customModule = new SimpleModule();


        // 注册自定义的序列化器和反序列化器
        customModule.addSerializer(Date.class, new DateSerializer());
        customModule.addDeserializer(Date.class, new DateDeserializer());


        // 注册自定义模块到ObjectMapper
        objectMapper.registerModule(customModule);


        // 序列化
        Person person = new Person("John Doe", new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"));
        String json = objectMapper.writeValueAsString(person);
        System.out.println(json);  
        // 输出:{"name":"John Doe","birthDate":"2000-01-01"}


        // 反序列化
        String jsonInput = "{\"name\":\"Jane Smith\",\"birthDate\":\"1990-12-31\"}";
        Person deserializedPerson = objectMapper.readValue(jsonInput, Person.class);
        System.out.println(deserializedPerson.getBirthDate());  
        // 输出:Sat Dec 31 00:00:00 GMT 1990
    }
}
  • 创建自定义的 SimpleModule,并在其中注册了自定义的序列化器和反序列化器。后将注册到 ObjectMapper 中,实现了对日期属性的自定义序列化和反序列化控制。

除了使用 SimpleModule,还可以通过实现自定义的 HandlerInstantiator 类来提供更复杂的定制化逻辑,以满足更高级的序列化和反序列化需求。HandlerInstantiator 可以用于创建自定义的序列化器、反序列化器、值处理器等。

五、使用BeanSerializerModifier

这个接口允许在序列化过程中动态地修改要应用的序列化器。通过实现这些接口,可根据特定的条件或者属性来动态地改变序列化器的行为。

这些方法提供了灵活的方式来实现自定义的序列化和反序列化控制,可以根据具体的需求选择最适合的方式来实现自定义行为。

  • BeanSerializerModifier 是一个抽象类,用于修改 BeanSerializer 的行为。
  • 继承 BeanSerializerModifier 类,并重写其中的方法来实现自定义的序列化控制。

使用 BeanSerializerModifier 来实现自定义的序列化控制:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;


import java.util.List;


public class CustomSerializationExample {


    public static class Person {
        private String name;
        private String email;


        // 省略构造函数和getter/setter方法
    }


    public static class UpperCaseSerializerModifier extends BeanSerializerModifier {
        @Override
        public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
            for (BeanPropertyWriter writer : beanProperties) {
                if (writer.getName().equals("email")) {
                    writer.assignSerializer(new UpperCaseStringSerializer());
                }
            }
            return beanProperties;
        }
    }


    public static class UpperCaseStringSerializer extends JsonSerializer<String> {
        @Override
        public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeString(value.toUpperCase());  // 自定义字符串序列化为大写形式
        }
    }


    public static void main(String[] args) {
        SimpleModule module = new SimpleModule("CustomModule");
        module.setSerializerModifier(new UpperCaseSerializerModifier());


        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(module);
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);


        Person person = new Person("John Doe", "john@example.com");


        try {
            String json = objectMapper.writeValueAsString(person);
            System.out.println(json);  
            // 输出:{"name":"John Doe","email":"JOHN@EXAMPLE.COM"}
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}
  • 创建自定义BeanSerializerModifier类UpperCaseSerializerModifier。在changeProperties方法中,检查属性名称是否为"email",如果是,将其序列化器指定为自定义的UpperCaseStringSerializer,以将email字段的值序列化为大写形式。然后将BeanSerializerModifier实例设置到SimpleModule中,并注册到ObjectMapper中。

六、使用案例:枚举、字典数据的自动转化

「实际使用场景」:java返回对象中关于枚举、字典数据的自动转化

6.1 实现思路

  • 1、通过自定义注解 对需要转化的字段进行标记,注解中可定义枚举类型,若没有定义枚举则从数据字典获取。
  • 2、自定义对象的BeanSerializerModifier,对做了标记的字段设置自定义的JsonSerializer。
  • 3、自定义JsonSerializer的实现。
  • 4、自定义MappingJackson2HttpMessageConverter,并设置自定义的BeanSerializerModifier为默认处理方式。
  • 5、将自定义的MappingJackson2HttpMessageConverter加入到HttpMessageConverters中,可以通过重写WebMvcConfigurationSupport.extendMessageConverters的方式实现。

6.2 代码实现

以下是具体的代码,有些地方需要根据实际情况自己实现,比如从字典获取数据等。

import java.lang.annotation.*;


@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {
    static enum Void {}
    Class<? extends Enum<?>> enumType() default Void.class;
    /**
     * 默认值,获取不到字典则使用默认值
     */
    String defaultValue() default "";
}

自定义BeanSerializerModifier,代码中DictConstants.getDictCacheKey(valueStr)只是自定义key,需要自己实现。

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.hg.dcm.commons.core.HGBusinessException;
import com.hg.dcm.commons.core.SpringContextUtil;
import com.hg.energy.service.RedisService;
import lombok.extern.slf4j.Slf4j;


import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;


@Slf4j
public class DictSerializerModifier extends BeanSerializerModifier {
    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
        for (BeanPropertyWriter beanProperty : beanProperties) {
            Dict dict = beanProperty.getAnnotation(Dict.class);
            if (dict != null) {
                beanProperty.assignSerializer(new DictSerializer(dict));
            }
        }
        return beanProperties;
    }


    /**
     *  字典自定义序列化
     */
    static class DictSerializer extends JsonSerializer<Object> {
        /**
         * 生成序列化字段后缀
         */
        private static final String LABEL_SUFFIX = "Desc";
        /**
         * 字典配置信息
         */
        private final Dict dict;


        /**
         * 枚举获取key方法
         */
        private static final String[] KEY_METHODS = {"getValue", "getCode", "getStatus", "name"};
        /**
         * 获取枚举描述方法
         */
        private static final String[] DESC_METHODS = {"getDesc"};


        /**
         *  构造方法
         *  @param dict 字典描述
         */
        public DictSerializer(Dict dict) {
            this.dict = dict;
        }


  
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            provider.defaultSerializeValue(value, gen);
            // 添加转换之后的字段:xxxDesc
            String fieldName = gen.getOutputContext().getCurrentName();
            gen.writeStringField(fieldName.concat(LABEL_SUFFIX), value != null ? this.getDesc(dict, value) : null);
        }




        /**
         * 获取字典信息
         * @param dict  字典对象
         * @param value 字典值
         */
        private String getDesc(Dict dict, Object value) {
            try {
                // 先查询是否是枚举类型,查到则返回
                String enumDesc = this.getEnumDesc(dict, value);
                if (enumDesc != null) {
                    return enumDesc;
                }
                String valueStr = Objects.toString(value);
                //获取缓存key,可以自定义
                String key = DictConstants.getDictCacheKey(valueStr);
                // Redis 缓存操作类 这里建议优先使用本地缓存, 本地缓存 -> redis -> Db
                RedisService redis = SpringContextUtil.getBean(RedisService.class);
                if (redis.exists(key)) {
                    return redis.get(key);
                }
                // 数据库字典操作类


                //redis.setEx(key, desc, 1, TimeUnit.HOURS);
                return "字典数据";
            } catch (Exception e) {
                log.error("字典转换:获取字典描述异常,使用默认值:{},key:{}, dict:{}, 异常:{}", dict.defaultValue(), value, dict.enumType(), e.getMessage(), e);
                return dict.defaultValue();
            }
        }




        /**
         * 获取枚举类型的描述信息
         *
         * @param dict  字典
         * @param value 值
         * @return 枚举desc字段
         */
        private String getEnumDesc(Dict dict, Object value) throws InvocationTargetException, IllegalAccessException {
            if (dict == null || value == null) {
                return null;
            }
            Class<? extends Enum<?>> et = dict.enumType();
            if (Dict.Void.class.equals(et)) {
                return null;
            }


            Enum<?>[] enums = et.getEnumConstants();
            Method keyMethod = this.getMethod(et, KEY_METHODS);
            if (keyMethod == null) {
                // 自定义业务异常
                throw new HGBusinessException(String.format("字典转换:枚举:%s,没有方法:%s", et.getName(), Arrays.toString(KEY_METHODS)));
            }
            Method descMethod = this.getMethod(et, DESC_METHODS);
            if (descMethod == null) {
                throw new HGBusinessException(String.format("字典转换:枚举:%s,没有方法:%s", et.getName(), Arrays.toString(DESC_METHODS)));
            }
            for (Enum<?> e : enums) {
                if (value.equals(keyMethod.invoke(e))) {
                    return Objects.toString(descMethod.invoke(e));
                }
            }
            log.error("字典转换:通过枚举转换失败,枚举:{},值:{},KeyMethod:{},DescMethod:{}", et.getName(), value, Arrays.toString(KEY_METHODS), Arrays.toString(DESC_METHODS));
            throw new HGBusinessException(String.format("字典转换失败,枚举:%s,值:%s", et.getName(), value));
        }


        /**
         * 获取读方法
         * @param enumType    枚举类
         * @param methodNames 方法名称
         * @return Method
         */
        private Method getMethod(Class<? extends Enum<?>> enumType, String... methodNames) {
            for (String methodName : methodNames) {
                try {
                    return enumType.getMethod(methodName);
                } catch (NoSuchMethodException e) {
                }
            }
            return null;
        }
    }
}

「自定义MappingJackson2HttpMessageConverter」

    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        SimpleModule simpleModule = new SimpleModule().setSerializerModifier(new DictSerializerModifier());
        builder.modules(simpleModule);
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        return new MappingJackson2HttpMessageConverter(builder.build());
    }

「将自定义的MappingJackson2HttpMessageConverter加入到HttpMessageConverters中」

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0,mappingJackson2HttpMessageConverter());
    }


    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        SimpleModule simpleModule = new SimpleModule().setSerializerModifier(new DictSerializerModifier());
        builder.modules(simpleModule);
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        return new MappingJackson2HttpMessageConverter(builder.build());
    }
}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表