Converter使用总结

1. 前言

本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:

  1. 存储
  2. 传输
  3. Spring机制
  4. 缓存组件

本篇是传输方面的第二篇,主要是Converter内容。Dto通过验证之后会进入Controller,下一步往往是将Dto进行convert,或直接存储,或用于其他业务。Converter做的事情就是将不同的Bean进行转换。

2. Converter方式

2.1 Converter

这种方式是1:1的转换方式,写在这里方便介绍概念

最直接的写法是在调用存储前,用DTO创建一个PO对象,然后进行存储。
spring提供了对这一过程的封装,称为converter,并且提供了对conterter调用的封装converterService

与Customize Validator的使用类似,也分成3步:1. 创建、2. 加载、3. 使用

  1. 创建一个Converter对象

    public class ProjectConverter implements Converter<ProjectDto, Project> {
    
        @Override
        public Project convert(ProjectDto projectDto) {
            ProjectState state = ProjectState.ON;
            if(projectDto.getState() != null){
                state = ProjectState.valueOf(projectDto.getState());
            }
            return new Project(projectDto.getId(), projectDto.getName(), projectDto.getDescription(), state);
        }
    }
    
  2. 将Conterter加载到converterService

    这一步有两种做法,一种是写xml文件,另一种是使用@Configuration

    xml文件方式:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="conversionService"
            class="org.springframework.context.support.ConversionServiceFactoryBean">
            <property name="converters">
                <set>
                    <bean class="com.example.converter.ProjectConverter"/>
                </set>
            </property>
        </bean>
    
    </beans>
    

    然后将xml放入配置

    @Configuration
    @ImportResource({"classpath*:applicationContext.xml"})
    public class XmlConfig {
    }
    

    直接写配置类方式:

    @Configuration
    public class AppConfig {
    
        @Bean
        public ConversionService conversionService() {
            DefaultConversionService conversionService = new DefaultConversionService();
            conversionService.addConverter(new ProjectFromDtoConverter());
            return conversionService;
        }
    }
    
    1. 使用
    @Service
    public class ProjectService {
        @Resource
        private ProjectDao projectDao;
    
        @Resource
        private ConversionService conversionService;
    
        @Override
        public Project save(ProjectDto projectDto) {
            Project project = conversionService.convert(projectDto, Project.class);
            projectDao.saveProject(project);
            return project;
        }
    }
    
    

2.2 ConverterFactor方式

如何对某一系列的类进行转换? 最常见的例子是对整个Enum的转换,spring提供了ConverterFactor。

它的定义

public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

S为Source,R为 Range意思是一系列的类的父类。 例子如下:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
        private Class<T> enumType;
        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }
        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

2.3 GenericConverter方式

如何将简单的DTO到PO的转换统一化?这时候每一种DTO都需要一个Converter来进行转换,有些啰嗦,
官方提供了GenericConverter通用的转换,可以完成一些通用的转换

定义如下:

public interface GenericConverter {
    public Set<ConvertiblePair> getConvertibleTypes();
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

更进一步还有ConditionalGenericConverter根据条件进行转换的情况,这里不介绍了。

2.4 小结

Converter不仅能完成对象到对象,也可以按成String到对象,Array到Array、String到Collection等各种转换功能,它比较强大。

对于特定场景的更适合对象到对象之间的转化

3. 对象之间转换

进行对象间的转换可以有3种方式:

3.1 反射方式

这种方式通过反射获取函数列表,并进行对比、判断、调用。需要注意命名的规范性。

  public static <S,T> void transalte(S s,T t){
        Method[] sourceMethods=s.getClass().getDeclaredMethods();
        Method[] targetMethods=t.getClass().getDeclaredMethods();
        for(Method m1:sourceMethods){
            if(m1.getName().startsWith("get")){
                String mNameSubfix=m1.getName().substring(3);
                String forName="set"+mNameSubfix;
                for(Method m2:targetMethods){
                    if(m2.getName().equals(forName)){
                        // 如果类型一致,或者m2的参数类型是m1的返回类型的父类或接口
                        boolean canContinue = m2.getParameterTypes()[0].isAssignableFrom(m1.getReturnType());
                        if (canContinue) {
                            try {
                                m2.invoke(t, m1.invoke(s));
                                break;
                            } catch (Exception e1) {
                                logger.debug("DTO 2 Entity转换失败");
                                e1.printStackTrace();
                            }
                        }
                    }
                }
            }

        }
        logger.debug("转换完成");
    }

这种方式通过判断getter与setter进行相应的转换

3.2 BeanUtils方式

Spring框架提供了基于BeanInfo的一种实现方式,原理与上一种类似。这种方式使用上更方便,还提供了忽略字段功能。

示例如下:

@PostMapping
public User addUser(UserInputDTO userInputDTO){
    User user = new User();
    BeanUtils.copyProperties(userInputDTO,user);

    return userService.addUser(user);
}

3.3 Json方式

还有一种实现方式,是基于对象与Json之间序列化的方式。通过Json实现两个类之间的变换。

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.jd.fastjson.JSON;
ObjectMapper objectMapper = new ObjectMapper();
//配置该objectMapper在反序列化时,忽略目标对象没有的属性。
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//读入需要更新的目标实体

ObjectReader objectReader = objectMapper.readerForUpdating(target);
//将源实体的值赋值到目标实体上
objectReader.readValue(JSON.toJSONString(source));

这里直接使用了JSON.toJSONString的方式来将source对象转化成json字符串,其实还可以通过writeValueAsString的方式,对序列化的字符串进行配置。如下

/*
 通过该方法对mapper对象进行设置,所有序列化的对象都将按改规则进行系列化
 Include.Include.ALWAYS 默认
 Include.NON_DEFAULT 属性为默认值不序列化
 Include.NON_EMPTY 属性为 空(“”) 或者为 NULL 都不序列化
 Include.NON_NULL 属性为NULL 不序列化
 */
  objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
  String outJson = objectMapper.writeValueAsString(productDetail);

前端可能只修改某几个属性,而在后端处理时也只希望处理这几个被赋值的属性,这时我们使用下面的方法。

@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public HttpEntity update(@PathVariable int id, @RequestBody ProductDetail productDetail)
  throws IOException {
 ProductDetail existing = repository.findById(id).get();
 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
 String outJson = objectMapper.writeValueAsString(productDetail);
 ObjectReader objectReader = objectMapper.readerForUpdating(existing);
 objectReader.readValue(outJson);
 repository.save(existing);
 return new ResponseEntity<>(existing, HttpStatus.ACCEPTED);
}

3.4 小结

从实践上看,大部分情况下使用BeanUtils方式,因为它使用了Spring框架提供的能力,效率更好一些,少数情况下可以使用Json方式,利用序列化与反序列化来完成。

JSON序列化有不同的库,Ali的FastJson、Google的Gson、以及Jackson,相关分析看这里

从使用体验上看,FastJson与Gson更好一些,性能上看Jackson会更好一些

4. 尾声

在一些简单的场景中,DTO与PO相同情况下是否可以将DTO与PO融合成一个呢?

数据到来之后,先验证,然后进入到controller中,再一步进行经过service、dao进行存储。从数据形态上上看,需要由DTO转变成PO。对于一些简单情况,我觉得DTO与PO是可以一个的,尤其以Mybatis做存储的时候。比如上文创建一个Person对象,Dto的属性与存储的属性相同,这时候是可以融合在一起的。

但更多的时候是不能融合的,比如想在save时增加一个createTime属性,逻辑是用户创建时自动生成,这时候DTO就不存在该属性。

分开写会使代码阅读起来更舒服一些。

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×