最近,项目的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表达式中调用外面的服务,这样也可以,毕竟每个人有每个人想法与用法,适合项目需求的才是最好的方法,能做到松耦合易扩展更加好。