spring boot项目实战之公共代码抽取
在一个web项目内,会有很多重复性的代码,如果将共性代码进行提取并规范使用,对后续运维和开发是很有帮助的。下面我们来看一下在一个web项目内有那些部分会出现诸多重复性的代码:
- model内:id、标记删除、创建时间、创建人、修改时间、修改人等字段
- service内:根据id查询、新增&修改、删除、分页查询等方法
- dao内: 根据id查询、新增&修改、删除、分页查询等方法
model
根据当前公司的情况,对model进行了以下抽象:
- BaseModel 接口,仅包含getId一个方法
- AbstractBaseModel 抽象类,继承BaseModel接口,提取了创建人、创建时间、修改人、修改时间字段
- MarkDeleteableModel 抽象类,继承AbstractBaseModel,增加了标记删除标记字段
代码如下:
public interface BaseModel<ID extends Serializable> extends Serializable{
ID getId();
}
/**
* 提取了部分公共字段,可以避免多个实体重复设置这些属性
*/
@MappedSuperclass
public abstract class AbstractBaseModel<ID extends Serializable> implements BaseModel<ID>{
private static final long serialVersionUID = 1195969732659409799L;
@ApiModelProperty(value="创建者ID")
@Column(columnDefinition="int default 0")
private int creator = 0;
@ApiModelProperty(value="创建时间")
@Column(name="create_time")
private Timestamp createTime;
@ApiModelProperty(value="最后修改人")
@Column(columnDefinition="int default 0")
private int updator = 0;
@ApiModelProperty(value="最后修改时间")
@Column(name="update_time")
private Timestamp updateTime;
//省略get set
}
/**
* 需要标记删除的实体继承该类
*/
@MappedSuperclass
public abstract class MarkDeleteableModel<ID extends Serializable> extends AbstractBaseModel<ID>{
private static final long serialVersionUID = -1880548221110317053L;
@ApiModelProperty(value="标记删除字段 0未删除 1已删除 ")
@Column(name = "del", columnDefinition = "tinyint default 0")
private int del = Constants.DEL_NO;
//省略get set
}
@MappedSuperclass用于说明该类不是某个实体,不和数据库的表相对应。
所有的实体皆继承以上三个类中的一个,数据需要标记删除的继承MarkDeleteableModel(如用户)、需要创建时间、修改时间的继承AbstractBaseModel(如角色),其它的实现BaseModel接口(如用户角色关系)
dao
公司项目使用的是spring boot + JPA,所以DAO层的公共操作已经在spring-data-jpa内进行了完善的抽象,如CrudRepository、PagingAndSortingRepository、JpaRepository、JpaSpecificationExecutor等接口,我们自己的DAO只需要根据需要进行继承就可以使用对应的查询方法。
但这里存在一个问题,当我抽取service层代码时肯定需要设置对应的dao,那就希望dao层能有一个统一的接口,如UserDao继承PagingAndSortingRepository和JpaRepository,UserRoleDao继承CrudRepository,虽然满足了对应实体的操作需要,但对service层代码的提取需求冲突,所以为了所有的DAO拥有相同的接口,因此我们可以提供一个公共DAO,满足绝大部分实体所需要的查询操作。
@NoRepositoryBean
public interface BaseDao<T,ID extends Serializable> extends JpaSpecificationExecutor<T>,JpaRepository<T, ID>{
}
- JpaRepository满足基本的增删改查、分页等需求
- JpaSpecificationExecutor满足多可选条件查询需求
service
service层主要提取通用的新增、修改、常用查询方法,这样在创建一个实体的service后,不需要在每个service的接口内都创建一遍这些同样的方法,就可以直接调用其上层方法进行操作。
public interface BaseService<T extends BaseModel<ID>, ID extends Serializable> {
/**
* 新增或更新
*/
T save(T t);
/**
* 新增或更新
* 注意数量不要太大,特别是数据迁移时不要使用该方法
*/
Iterable<T> save(Iterable<T> entities);
/**
* 根据ID删除
*/
void del(ID id);
/**
* 根据实体删除
*/
void del(T t);
/**
* 根据ID查找对象
*/
T findById(ID id);
List<T> findAll();
/**
* 分页排序获取数据
* 禁止使用该接口进行count操作
* Pageable pageable = new PageRequest(0, 10, new Sort(Sort.Direction.DESC,"id"));
* @param pageable
* @return
*/
Page<T> findAll(Pageable pageable);
/**
* 多条件查询
* 注:多个条件间是and关系 & 参数是属性对应的类型 使用时注意避免结果集过大
* @author yangwk
* @time 2017年8月1日 下午3:50:46
* @param params {"username:like":"test"} 键的格式为字段名:过滤方式,过滤方式见{@code QueryTypeEnum}
* @return
*/
List<T> list(Map<String, Object> params);
/**
* 分页多条件查询
* 注:多个条件间是and关系 & 参数是属性对应的类型
* @author yangwk
* @time 2017年8月1日 下午3:50:46
* @param params {"username:like":"test"} 键的格式为字段名:过滤方式,过滤方式见{@code QueryTypeEnum}
* @param pageable 分页信息 new PageRequest(page, size,new Sort(Direction.DESC, "updateTime"))
* @return
*/
Page<T> list(Map<String, Object> params,Pageable pageable);
}
如上,BaseService内提供了save、del、findAll、list等常用方法,注释比较清楚,可查看注释了解其具体作用。
@SuppressWarnings("unchecked")
public abstract class BaseServiceImpl<T extends BaseModel<ID>, ID extends Serializable> implements BaseService<T, ID> {
public abstract BaseDao<T, ID> getDAO();
@Override
public T save(T t){
if(t instanceof AbstractBaseModel){
AbstractBaseModel<T> baseModel = (AbstractBaseModel<T>)t;
Timestamp current = new Timestamp(System.currentTimeMillis());
if(baseModel.getId() == null){
baseModel.setCreateTime(current);
}
baseModel.setUpdateTime(current);
}
return getDAO().save(t);
}
@Override
public Iterable<T> save(Iterable<T> entities){
for (T t : entities) {
if(t instanceof AbstractBaseModel){
AbstractBaseModel<T> baseModel = (AbstractBaseModel<T>)t;
Timestamp current = new Timestamp(System.currentTimeMillis());
if(baseModel.getId() == null){
baseModel.setCreateTime(current);
}
baseModel.setUpdateTime(current);
}
}
return getDAO().save(entities);
}
@Override
public void del(ID id){
T t = findById(id);
if(t == null){
return;
}
if(t instanceof MarkDeleteableModel){
MarkDeleteableModel<T> baseModel = (MarkDeleteableModel<T>)t;
baseModel.setUpdateTime(new Timestamp(System.currentTimeMillis()));
baseModel.setDel(1);
getDAO().save(t);
} else {
getDAO().delete(t);;
}
}
@Override
public void del(T t) {
if(t instanceof MarkDeleteableModel){
MarkDeleteableModel<T> baseModel = (MarkDeleteableModel<T>)t;
baseModel.setUpdateTime(new Timestamp(System.currentTimeMillis()));
baseModel.setDel(1);
getDAO().save(t);
} else {
getDAO().delete(t);
}
}
@Override
public T findById(ID id) {
return getDAO().findOne(id);
}
@Override
public List<T> findAll() {
return getDAO().findAll();
}
@Override
public Page<T> findAll(Pageable pageable){
return getDAO().findAll(pageable);
}
@Override
public List<T> list(final Map<String, Object> params) {
Specification<T> spec = new Specification<T>() {
@Override
public Predicate toPredicate(Root<T> root,CriteriaQuery<?> query,CriteriaBuilder cb) {
List<Predicate> list = new ArrayList<Predicate>();
for(Entry<String, Object> entry : params.entrySet()){
Object value = entry.getValue();
if(value == null || StringUtils.isBlank(value.toString())){
continue;
}
String key = entry.getKey();
String[] arr = key.split(":");
Predicate predicate = getPredicate(arr,value,root,cb);
list.add(predicate);
}
Predicate[] p = new Predicate[list.size()];
return cb.and(list.toArray(p));
}
};
List<T> list = getDAO().findAll(spec);
return list;
}
@Override
public Page<T> list(final Map<String, Object> params,Pageable pageable){
Specification<T> spec = new Specification<T>() {
@Override
public Predicate toPredicate(Root<T> root,CriteriaQuery<?> query,CriteriaBuilder cb) {
List<Predicate> list = new ArrayList<Predicate>();
for(Entry<String, Object> entry : params.entrySet()){
Object value = entry.getValue();
if(value == null || StringUtils.isBlank(value.toString())){
continue;
}
String key = entry.getKey();
String[] arr = key.split(":");
Predicate predicate = getPredicate(arr,value,root,cb);
list.add(predicate);
}
Predicate[] p = new Predicate[list.size()];
return cb.and(list.toArray(p));
}
};
Page<T> page = getDAO().findAll(spec, pageable);
return page;
}
private Predicate getPredicate(String[] arr, Object value,
Root<T> root, CriteriaBuilder cb) {
if(arr.length == 1){
return cb.equal(root.get(arr[0]).as(value.getClass()), value);
}
if(QueryTypeEnum.like.name().equals(arr[1])){
return cb.like(root.get(arr[0]).as(String.class), String.format("%%%s%%", value));
}
if(QueryTypeEnum.ne.name().equals(arr[1])){
return cb.notEqual(root.get(arr[0]).as(value.getClass()), value);
}
if(QueryTypeEnum.lt.name().equals(arr[1])){
return getLessThanPredicate(arr,value,root,cb);
}
if(QueryTypeEnum.lte.name().equals(arr[1])){
return getLessThanOrEqualToPredicate(arr,value,root,cb);
}
if(QueryTypeEnum.gt.name().equals(arr[1])){
return getGreaterThanPredicate(arr,value,root,cb);
}
if(QueryTypeEnum.gte.name().equals(arr[1])){
return getGreaterThanOrEqualToPredicate(arr,value,root,cb);
}
throw new UnsupportedOperationException(String.format("不支持的查询类型[%s]",arr[1]));
}
private Predicate getLessThanPredicate(String[] arr, Object value,
Root<T> root, CriteriaBuilder cb) {
if(Integer.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Integer.class), (int)value);
}
if(Long.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Long.class), (long)value);
}
if(Double.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Double.class), (double)value);
}
if(Float.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Float.class), (float)value);
}
if(Timestamp.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
}
if(Date.class == value.getClass()){
return cb.lessThan(root.get(arr[0]).as(Date.class), (Date)value);
}
return cb.lessThan(root.get(arr[0]).as(String.class), String.valueOf(value));
}
private Predicate getLessThanOrEqualToPredicate(String[] arr,
Object value, Root<T> root, CriteriaBuilder cb) {
if(Integer.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Integer.class), (int)value);
}
if(Long.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Long.class), (long)value);
}
if(Double.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Double.class), (double)value);
}
if(Float.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Float.class), (float)value);
}
if(Timestamp.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
}
if(Date.class == value.getClass()){
return cb.lessThanOrEqualTo(root.get(arr[0]).as(Date.class), (Date)value);
}
return cb.lessThanOrEqualTo(root.get(arr[0]).as(String.class), String.valueOf(value));
}
private Predicate getGreaterThanPredicate(String[] arr,
Object value, Root<T> root, CriteriaBuilder cb) {
if(Integer.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Integer.class), (int)value);
}
if(Long.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Long.class), (long)value);
}
if(Double.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Double.class), (double)value);
}
if(Float.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Float.class), (float)value);
}
if(Timestamp.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
}
if(Date.class == value.getClass()){
return cb.greaterThan(root.get(arr[0]).as(Date.class), (Date)value);
}
return cb.greaterThan(root.get(arr[0]).as(String.class), String.valueOf(value));
}
private Predicate getGreaterThanOrEqualToPredicate(String[] arr,Object value,
Root<T> root, CriteriaBuilder cb) {
if(Integer.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Integer.class), (int)value);
}
if(Long.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Long.class), (long)value);
}
if(Double.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Double.class), (double)value);
}
if(Float.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Float.class), (float)value);
}
if(Timestamp.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
}
if(Date.class == value.getClass()){
return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Date.class), (Date)value);
}
return cb.lessThanOrEqualTo(root.get(arr[0]).as(String.class), String.valueOf(value));
}
}
@ApiModel(value="查询条件支持的过滤方式")
public enum QueryTypeEnum {
like,
equal,
ne,
lt,
lte,
gt,
gte
}
每个service都继承BaseService接口,每个service实现类都继承BaseServiceImpl抽象类,提取方法时有两点可能需要注意:
- 哪些方法是自己项目内常用的---确定提取方法范围
- getDAO()抽象方法,要求实现类返回一个统一的BaseDao,方便数据操作;
使用示例:
public class User implements BaseModel<Integer>{}
public interface UserDao extends BaseDao<User, Integer>{}
public interface UserService extends BaseService<User, Integer>{}
public class UserServiceImpl extends BaseServiceImpl<User, Integer> implements UserService{
@Autowired
private UserDao userDao;
@Override
public BaseDao<User, Integer> getDAO() {
return userDao;
}
}
小结
本文属于抛砖引玉,希望能让你了解大量重复的代码出现在项目内对开发效率和维护来说有不少的负面影响,应该通过多种方式消除重复性的代码,本文列举的提取上层model、dao、service是其中的一个思路,你可以参考本文的实现,结合自己项目的情况进行尝试,效果基本是正向的。
web基础开发框架搭建可参考https://github.com/q7322068/rest-base,整合了web开发需要的基本功能,本文代码皆包含在该项目内。