SpringBoot:分页数据的类型转换在2.x与1.x版本的区别

最近,项目的springboot版本从1.5.22.release版本升级到2.x版本时,发现2.x版本的springboot与1.x版本的代码有一定的变化,导致在编译代码时出现了不能编译通过的代码错误提示。

在修复代码以适应2.x版SpringBoot框架时,其中有一项是关于分页的错误:不能将分页的数据类型转换成另一种类型。

Spring提供了JPA【Java Persistence API:基于O/R映射的标准规范】通用接口去从数据库获取数据,且对分页PagingAndSortingRepository接口提供了JPA实现【SimpleJpaRepository】,项目通过此接口的findAll(Pageable pageable)方法从数据库获取分页数据后会组装成分页对象Page<T>返回如下:

Page<T>中的类型是映射数据库中的表的实体对象,我们在页面上显示时是不需要显示表中所有的字段的,因此我们需要将T类型的对象转换成另一种类型,这时候,我们会用到Page这个接口中的一个映射方法map(), 在SpringBoot 2.x版本与1.x版本中,这个方法的两个参数类型不一样,旧版是Converter接口,方法是convert(),新版是函数式接口Function,方法是apply(),导致出现了不兼容的问题。如下所示:

SpringBoot1.x升级到2.x导致分页数据类型转换存在问题的解决办法

1.  将类型转换实现类实现的SpringBoot 1.x版本中Converter接口类改为函数式接口Function,实现方法也用apply()即可解决升级SpringBoot 2.x版本带来的分页类型转换不兼容问题。

2.  直接用Lambda表达式直接对分页中的数据做类型转换。【请跳到“最好的一种解决办法”那个部分


扩展功能:如何在类型转换时做一些业务处理?【最后有更加简洁的业务处理方法】

由于业务需要在从一张表中获取分页数据后,将分页中的数据做业务逻辑的处理,最后再转换成另一个类型的对象,作为分页的显示数据,因此,在SpringBoot 1.x版本时,在项目中实现了接口Converter,在Converter的实现类中提供方法来注入做业务逻辑处理的实现类【策略设计模式,不同的表的分页就可以处理自己的业务】,在实现方法convert()中嵌入了调用实现业务逻辑的接口,然后返回业务结果的对象给Page对象进行封装分页显示数据。实现代码如下所示:

1. Connverter的实现类ObjectMapConverter.java

package com.crh.demo.converter;

import com.crh.demo.service.convert.ConvertService;
import com.crh.demo.util.JsonUtils;

import org.springframework.core.convert.converter.Converter;

/**
 * Custom converter is used to convert type for pagination.
 *
 * @param <S> the type of source object.
 * @param <T> the type of target object.
 * @author crh
 */
public class ObjectMapConverter<S, T> implements Converter<S, T> {

    /**
     * 指定的转换后的类型。
     */
    private Class<T> returnType;

    /**
     * 转换服务,业务逻辑会在此服务的接口中实现。
     */
    private ConvertService<S, T> convertService;

    /**
     * 获取转换的服务对象。
     *
     * @return 转换的服务对象
     */
    public ConvertService<S, T> getConvertService() {
        return convertService;
    }

    /**
     * 设置转换的服务对象,此处使用不同的服务对象达到处理不同种类的分页数据的业务逻辑。
     *
     * @param convertService the convert service.
     */
    public void setConvertService(ConvertService<S, T> convertService) {
        this.convertService = convertService;
    }

    /**
     * 构造函数中指定将分页数据转换的类型。
     * @param returnType the returned type <T>
     */
    public ObjectMapConverter(Class<T> returnType) {
        this.returnType = returnType;
    }

    @Override
    public T convert(S s) {
        if (convertService != null) {
            //如果指定了处理业务的转换服务,则开始处理业务,再转换。
            return convertService.convert(s);
        } else {
            //没有指定处理业务的服务,则用alibaba的fastjson进行类型转换。
            return JsonUtils.mapObject(s, this.returnType);
        }
    }
}

2. 定义转换时的业务逻辑处理接口类ConvertService.java

package com.crh.demo.service.convert;

/**
 * Interface for Convert Service, which is used to convert between two class types.
 *
 * @author crh
 */
public interface ConvertService<S, T> {

    T convert(S s);
}

3. 定义转换类型时的业务逻辑处理实现类UserLoginInfoConvertServiceImpl.java

package com.crh.demo.service.convert.impl;

import com.crh.demo.constants.Constants;
import com.crh.demo.dao.UserRepository;
import com.crh.demo.dto.UserLoginInfo;
import com.crh.demo.entity.LoginInfo;
import com.crh.demo.service.convert.ConvertService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Implementation of {@link ConvertService}, which is used to convert between two class types {@link LoginInfo}
 * {@link UserLoginInfo}.
 *
 * @author crh
 */
@Service(Constants.Service.TYPE_USER_LOGIN_INFO_CONVERT)
public class UserLoginInfoConvertServiceImpl implements ConvertService<LoginInfo, UserLoginInfo> {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserLoginInfo convert(LoginInfo loginInfo) {
        //此处用来将分页中的数据做相关的业务逻辑,最后组装成返回的类型的数据。
        UserLoginInfo userLoginInfo = new UserLoginInfo();
        userLoginInfo.setUserInfo(userRepository.findById(loginInfo.getUserId()).get());
        userLoginInfo.setLoginInfo(loginInfo);
        return userLoginInfo;
    }
}

4. 获取分页数据的接口类UserService.java

package com.crh.demo.service.register;

import com.crh.demo.dto.UserLoginInfo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

/**
 * Interface for User service, which is used to retrieve the user login info.
 *
 * @author crh
 */
public interface UserService {

    /**
     * Retrieve the user login info pagination by login type and page request object.
     *
     * @param loginType   the login type.
     * @param pageRequest page request object.
     * @return User Login Info Pagination
     */
    Page<UserLoginInfo> retrieveUserLoginInfoPage(String loginType, PageRequest pageRequest);

    /**
     * Retrieve the user login info pagination by page request object.
     *
     * @param pageRequest page request object.
     * @return User Login Info Pagination
     */
    Page<UserLoginInfo> retrieveUserLoginInfoPage(PageRequest pageRequest);

}

5. 获取分页数据的实现类UserServiceImpl.java

package com.crh.demo.service.register.impl;

import com.crh.demo.constants.Constants;
import com.crh.demo.converter.ObjectMapConverter;
import com.crh.demo.dao.LoginRepository;
import com.crh.demo.dao.UserRepository;
import com.crh.demo.dto.UserLoginInfo;
import com.crh.demo.entity.LoginInfo;
import com.crh.demo.service.convert.ConvertService;
import com.crh.demo.service.register.UserService;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import java.util.Map;

/**
 * Implementation of {@link UserService}, which is used to retrieve the user login info.
 *
 * @author crh
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private LoginRepository loginRepository;

    @Autowired
    private Map<String, ConvertService> convertServiceMap = Maps.newHashMap();

    @Override
    public Page<UserLoginInfo> retrieveUserLoginInfoPage(String loginType, PageRequest pageRequest) {
        //查询条件构造
        Specification<LoginInfo> spec = (root, query, cb) -> {
            Path<String> type = root.get("LoginInfo");
            Predicate p = cb.equal(type, loginType);
            return p;
        };

        //get the login data.
        Page<LoginInfo> loginInfoPage = loginRepository.findAll(spec, pageRequest);

        //转换分页的数据类型
        return convertPageWithLoginInfo(loginInfoPage);
    }

    @Override
    public Page<UserLoginInfo> retrieveUserLoginInfoPage(PageRequest pageRequest) {
        //get the login data.
        Page<LoginInfo> loginInfoPage = loginRepository.findAll(pageRequest);

        //转换分页的数据类型
        return convertPageWithLoginInfo(loginInfoPage);
    }

    /**
     * Convert the login info of page data to user login info object.
     *
     * @return the pagination with user login info type.
     */
    private Page<UserLoginInfo> convertPageWithLoginInfo(Page<LoginInfo> loginInfoPage) {
        //构建分页数据类型转换实现类
        ObjectMapConverter<LoginInfo, UserLoginInfo> objectMapConverter = new ObjectMapConverter<LoginInfo, UserLoginInfo>(UserLoginInfo.class);
        //指定分页数据的业务逻辑处理类
        objectMapConverter.setConvertService(convertServiceMap.get(Constants.Service.TYPE_USER_LOGIN_INFO_CONVERT));

        //绑定分页数据类型转换关系,并开始转换数据。
        return loginInfoPage.map(objectMapConverter);
    }
}

6. 其它在上面的实现过程中用到的类如下:

UserInfo.java

package com.crh.demo.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Table;

/**
 * User entity is used to map the user info with db table 'tb_user'.
 *
 * @author crh
 */
@Data
@Entity
@Table(name = "tb_user")
@EntityListeners(AuditingEntityListener.class)
@EqualsAndHashCode(callSuper = true)
public class UserInfo extends BaseEntity {

    /**
     * 用户ID
     */
    @Column
    private String userId;

    /**
     * 名字
     */
    @Column
    private String name;

    /**
     * 性别
     */
    @Column
    private String sex;

    /**
     * 年龄
     */
    @Column
    private int age;

}

LoginInfo.java

package com.crh.demo.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Table;

/**
 * User Login entity is used to map the user login info with db table 'tb_login'.
 *
 * @author crh
 */
@Data
@Entity
@Table(name = "tb_login")
@EntityListeners(AuditingEntityListener.class)
@EqualsAndHashCode(callSuper = true)
public class LoginInfo extends BaseEntity {

    /**
     * 用户ID
     */
    @Column
    private long userId;

    /**
     * 登录类型
     */
    @Column
    private String type;

    /**
     * 登录IP
     */
    @Column
    private String ip;

    /**
     * 登录时间
     */
    @Column
    private String loginTime;
}

BaseEntity.java

package com.crh.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * Base entity is used to map the common column.
 *
 * @author crh
 */
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {

    private static final long serialVersionUID = -4620298382478282920L;

    /**
     * PK - ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    private Date createdDate;

    @CreatedBy
    private String createdBy;

    @LastModifiedDate
    private Date updatedDate;

    @LastModifiedBy
    private String updatedBy;

    @Column
    private Boolean isDelete = false;

    @Version
    private Integer version;

}

UserLoginInfo.java

package com.crh.demo.dto;

import com.crh.demo.entity.LoginInfo;
import com.crh.demo.entity.UserInfo;
import lombok.Data;

import java.io.Serializable;

/**
 * User login info data transfer object, which is used to as context for storing
 * user info, user login info.
 *
 * @author crh
 */
@Data
public class UserLoginInfo implements Serializable {

    private static final long serialVersionUID = -1292921159997767935L;

    /**
     * 用户信息
     */
    private UserInfo userInfo;

    /**
     * 用户登录信息
     */
    private LoginInfo loginInfo;

}

UserRepository.java

package com.crh.demo.dao;

import com.crh.demo.entity.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Extended JPA repository class is used to operation entity {@link UserInfo}.
 *
 * @author crh
 */
@Repository
public interface UserRepository extends JpaRepository<UserInfo, Long>, JpaSpecificationExecutor<UserInfo> {

    /**
     * Find the user info without deleted.
     * @param isDelete if it's deleted.
     * @return the list of user info.
     */
    List<UserInfo> findByIsDelete(Boolean isDelete);
}

LoginRepository.java

package com.crh.demo.dao;

import com.crh.demo.entity.LoginInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Extended JPA repository class is used to operation entity {@link LoginInfo}.
 *
 * @author crh
 */
@Repository
public interface LoginRepository extends JpaRepository<LoginInfo, Long>, JpaSpecificationExecutor<LoginInfo> {

    /**
     * Find the login info without deleted.
     *
     * @param isDelete if it's deleted.
     * @return the list of login info.
     */
    List<LoginInfo> findByIsDelete(Boolean isDelete);
}

Constants.java

package com.crh.demo.constants;

/**
 * This class is used to define the constants.
 *
 * @author crh
 */
public interface Constants {

    interface Service {
        String TYPE_USER_LOGIN_INFO_CONVERT = "userLoginInfoConvert";
    }

}

JsonUtils.java

package com.crh.demo.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.TypeReference;

import java.util.List;
import java.util.Map;

/**
 * This utility is used to handle the conversion between json string and class object.
 *
 * @author crh
 */
public class JsonUtils {

    private JsonUtils() {
    }

    /**
     * Convert the object to the json string.
     *
     * @param <T> the parameter type T.
     * @return the json string.
     */
    public static <T> String toJsonString(T t) {
        return JSON.toJSONString(t);
    }

    /**
     * Convert json string to the object with the parameter class type.
     *
     * @param json json string
     * @param type the parameter class type object
     * @param <T>  the parameter type T.
     * @return the object of parameter type
     */
    public static <T> T parseObject(String json, Class<T> type) {
        return JSON.parseObject(json, type);
    }

    /**
     * Convert json string to the object list with the parameter class type.
     *
     * @param json json string
     * @param type the parameter class type object
     * @param <T>  the parameter type T.
     * @return the object list with the parameter type
     */
    public static <T> List<T> parseObjectList(String json, Class<T> type) {
        return JSON.parseObject(json, new TypeReference<List<T>>(type) {
        });
    }

    /**
     * Convert json string to map object with the parameter class type K, V.
     *
     * @param json      json string
     * @param keyType   the parameter class type object for key
     * @param valueType the parameter class type object for value
     * @param <K>       the parameter class type K for key
     * @param <V>       the parameter class type V for value
     * @return the map object with the parameter type K, V.
     */
    public static <K, V> Map<K, V> parseMap(String json, Class<K> keyType, Class<V> valueType) {
        return JSON.parseObject(json, new TypeReference<Map<K, V>>(keyType, valueType) {
        });
    }

    /**
     * Convert the object list with the parameter type T to the object list with the parameter type R.
     *
     * @param tList the object list with the parameter type T
     * @param type  the parameter class type object
     * @param <T>   the parameter class type T
     * @param <R>   the parameter class type R
     * @return the object list with the parameter type T.
     */
    public static <T, R> List<R> mapObjectList(List<T> tList, Class<R> type) {
        return JSON.parseObject(JSON.toJSONString(tList), new TypeReference<List<R>>(type) {
        });
    }

    /**
     * Convert the object with the parameter type T to the object with the parameter type R.
     *
     * @param t    the object with the parameter type T
     * @param type the parameter class type object
     * @param <T>  the parameter class type T
     * @param <R>  the parameter class type R
     * @return the object with the parameter type R.
     */
    public static <T, R> R mapObject(T t, Class<R> type) {
        return JSON.parseObject(JSON.toJSONString(t), type);
    }

    /**
     * Convert the object with the parameter type T to the object with the parameter type R.
     *
     * @param json the json
     * @return true or false
     */
    public static boolean isArrayType(String json) {
        Object object = JSON.parse(json);
        if (object instanceof JSONArray) {
            return true;
        } else {
            return false;
        }
    }

}

切换到2.x之后,只要修改ObjectMapConverter.java的实现接口为Function.java接口即可解决问题,代码如下:

package com.crh.demo.converter;

import com.crh.demo.service.convert.ConvertService;
import com.crh.demo.util.JsonUtils;

import java.util.function.Function;

/**
 * Custom converter is used to convert type for pagination.
 *
 * @param <S> the type of source object.
 * @param <T> the type of target object.
 * @author crh
 */
public class ObjectMapConverter<S, T> implements Function<S, T> {

    /**
     * 指定的转换后的类型。
     */
    private Class<T> returnType;

    /**
     * 转换服务,业务逻辑会在此服务的接口中实现。
     */
    private ConvertService<S, T> convertService;

    /**
     * 获取转换的服务对象。
     *
     * @return 转换的服务对象
     */
    public ConvertService<S, T> getConvertService() {
        return convertService;
    }

    /**
     * 设置转换的服务对象,此处使用不同的服务对象达到处理不同种类的分页数据的业务逻辑。
     *
     * @param convertService the convert service.
     */
    public void setConvertService(ConvertService<S, T> convertService) {
        this.convertService = convertService;
    }

    /**
     * 构造函数中指定将分页数据转换的类型。
     * @param returnType the returned type <T>
     */
    public ObjectMapConverter(Class<T> returnType) {
        this.returnType = returnType;
    }

    @Override
    public T apply(S s) {
        if (convertService != null) {
            //如果指定了处理业务的转换服务,则开始处理业务,再转换。
            return convertService.convert(s);
        } else {
            //没有指定处理业务的服务,则用alibaba的fastjson进行类型转换。
            return JsonUtils.mapObject(s, this.returnType);
        }
    }
}

最好的一种解决办法更加简洁的写法:用Lambda表达式,估计这应该也是SpringBoot 2.x将参数改为函数式接口的初衷:

UserServiceImpl.java

package com.crh.demo.service.register.impl;

import com.crh.demo.constants.Constants;
import com.crh.demo.converter.ObjectMapConverter;
import com.crh.demo.dao.LoginRepository;
import com.crh.demo.dao.UserRepository;
import com.crh.demo.dto.UserLoginInfo;
import com.crh.demo.entity.LoginInfo;
import com.crh.demo.service.convert.ConvertService;
import com.crh.demo.service.register.UserService;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import java.util.Map;

/**
 * Implementation of {@link UserService}, which is used to retrieve the user login info.
 *
 * @author crh
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private LoginRepository loginRepository;

    @Autowired
    private Map<String, ConvertService> convertServiceMap = Maps.newHashMap();

    @Override
    public Page<UserLoginInfo> retrieveUserLoginInfoPage(String loginType, PageRequest pageRequest) {
        //查询条件构造
        Specification<LoginInfo> spec = (root, query, cb) -> {
            Path<String> type = root.get("LoginInfo");
            Predicate p = cb.equal(type, loginType);
            return p;
        };

        //get the login data.
        Page<LoginInfo> loginInfoPage = loginRepository.findAll(spec, pageRequest);

        //转换分页的数据类型
        return convertPageWithLoginInfo(loginInfoPage);
    }

    @Override
    public Page<UserLoginInfo> retrieveUserLoginInfoPage(PageRequest pageRequest) {
        //get the login data.
        Page<LoginInfo> loginInfoPage = loginRepository.findAll(pageRequest);

        //转换分页的数据类型
        return convertPageWithLoginInfo(loginInfoPage);
    }

    /**
     * Convert the login info of page data to user login info object.
     *
     * @return the pagination with user login info type.
     */
    private Page<UserLoginInfo> convertPageWithLoginInfo(Page<LoginInfo> loginInfoPage) {
        //用Lambda表达式写转换数据类型的逻辑或者做相关的业务处理后再转换类型。
        return loginInfoPage.map(loginInfo -> {
            UserLoginInfo userLoginInfo = new UserLoginInfo();
            userLoginInfo.setUserInfo(userRepository.findById(loginInfo.getUserId()).get());
            userLoginInfo.setLoginInfo(loginInfo);
            return userLoginInfo;
        });
    }
}

 

总结:

在Page接口类中的map()方法从SpringBoot 1.x版本到SpringBoot 2.x 版本的变化在于参数上面由Converter接口变成了函数式接口Function,以前是需要先实现一个Converter,然后用这个Converter去做数据转换,现在可以直接用Lambda表达式来直接写转换的业务逻辑,省去了写实现类的过程,代码会少很多,比较简洁,但是如果需要业务处理的话,以后可能需要业务扩展,最好还是实现函数式接口来达到处理逻辑业务的目的。有人可能会说,你外面定义一个服务,在Lambda表达式中调用外面的服务,这样也可以,毕竟每个人有每个人想法与用法,适合项目需求的才是最好的方法,能做到松耦合易扩展更加好。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值