Spring Boot项目通用功能第一讲之《通用Service》

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aiyaya_/article/details/79212852

前言

上两篇文章中我们说了下怎么去做《通用Mapper》《通用分页》,来简化单表操作下的DAO层的逻辑,然而我们的目标仅仅是这样么?显然不是,本章为你讲解项目中通用的service的抽离,用于简化你的业务逻辑层,愿你能在其中得到启发和深入思考。

思考

首先我们先思考一下,对于SERVICE层有哪些是可以被公共出来的东西?
好,我先来说几个:
1. 对于单张表的增、删、改、查(单条查、批量查、分页查)功能
2. 单张表的增、删、改、查功能相对应的缓存功能(缓存有很多方式:ehcache缓存、redis缓存等等)
3. 对数状结构的表操作(树状结构比较常见,比如:后台权限用到的资源树表、地域城市表、组织架构表等等的业务场景)
4. 很多的主功能业务表中可能会增加一个扩展字段用于存储附加属性,那如果我们把这种key->value结构关系单独抽离一个功能,来简化我们对这种附加属性的使用,是不是会更好呢(比如:在order表中,总是有一些并不是所有订单都会有的属性,这时我们可能会将这些属性存储在一个叫ext的字段中,格式可能是json格式的字符串也可能是key:value这种数据结构,但是想想这种方式的存储是不是很不利于结构化搜索,而且还要在主表上维护扩展属性的增删改查功能,抽离下公共功能的附加表是个很好的方式)

好了,上面说了几点关于业务逻辑层我们可能会有哪些公共的逻辑可以被抽离,在企业开发中,可不仅仅是这样,对于不同的公司业务也都有很大差异,所以你也可以向我一样思考一下,在你的企业里,有哪些像上述所说的功能可以被抽离的呢?

实现上述思考

因为篇幅有限,所以对于通用service我们会分几篇文章来详细讲解下,本章我们只说一下我们的思考1:对于单张表的增、删、改、查功能
那好,我们在紧接着想想,既然是要抽离单张表增删改查SERVICE层,那我们肯定是要有一个相对通用的DAO层来完成与数据库的沟通了,没错,我们就是借助上篇讲到的通用Mapper来实现的。
下面让我们来看下具体的代码实现。

PO类统一接口:

package com.zhuma.demo.comm.model.po;

import com.zhuma.demo.comm.model.Model;

import java.util.Date;

public interface PO<PK> extends Model {

    PK getId();

    void setId(PK id);

    Date getCreateTime();

    void setCreateTime(Date createTime);

    Date getUpdateTime();

    void setUpdateTime(Date updateTime);
}

备注

  • PO类(也就是数据库表对应的实体类)的统一接口,所有的PO类都应该实现该接口。
  • 我们的实体类目录会分的相对详细些:po(persistant object 持久对象)、qo(query object查询对象)、vo(view object 值对象)、bo(business object 业务对象)。
  • 从该类中可以看出如果你是PO类需要至少三个参数,id:主键、createTime:创建时间 、updateTime:更新时间,之所以将它们三个都抽离因为这在绝大多数的po下都是公用的,如果你想说我的表就是一个日志记录表都没有更新的情况,所以也没有updateTime,这里,多加个字段无伤大雅,定义这几个参数好处后面会说下。

BasePO类:

package com.zhuma.demo.comm.model.po;

import java.util.Date;

import javax.persistence.Column;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @desc 基础PO类

 * @author zhuamer
 * @since 7/3/2017 2:14 PM
 */
@Data
public abstract class BasePO<PK> implements PO<PK> {

    @ApiModelProperty(value = "创建时间")
    @Column(name = "create_time")
    private Date createTime;

    @ApiModelProperty(value = "更新时间")
    @Column(name = "update_time")
    private Date updateTime;

}

备注

  • 该类是上面PO接口的一个部分实现,如果你懒得定义createTime、updateTime字段可以让你的持久化类直接继承该类即可。

通用接口定义:

package com.zhuma.demo.comm.service;

/**
 * @desc 通用服务类
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
public interface CrudService<E, PK> extends
        InsertService<E, PK>,
        UpdateService<E,PK>,
        DeleteService<PK>,
        SelectService<E, PK> {
}

解释说明

  • 该接口是本章所讲的核心接口类,看名字可以看出Crud就代表该接口具有增删改查的功能,它的能力也都是继承相应的具体接口得来。
  • 业务操作service接口类如果想使用通用service的功能,都应该继承该接口以获得相应方法能力。
  • 使用时需要传递两个泛型值,E: 具体的PO类, PK:PO类的主键类型。

通用增加接口:

package com.zhuma.demo.comm.service;

/**
 * @desc 基础插入服务
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
public interface InsertService<E, PK> {

    /**
     * 添加一条数据
     *
     * @param record 要添加的数据
     * @return 添加后生成的主键
     */
    PK insert(E record);
}

通用删除接口:

package com.zhuma.demo.comm.service;

/**
 * @desc 基础删除服务
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
public interface DeleteService<PK> {

    /**
     * 根据主键删除记录
     *
     * @param pk 主键
     * @return 影响记录数
     */
    int deleteByPk(PK pk);

    /**
     * 根据主键删除记录
     *
     * @param pks 主键集合
     * @return 影响记录数
     */
    int deleteByPks(Iterable<PK> pks);
}

通用修改接口:

package com.zhuma.demo.comm.service;

/**
 * @desc 基础更新服务
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
public interface UpdateService<E, PK> {
    /**
     * 修改记录信息
     *
     * @param pk 主键
     * @param record 要修改的对象
     * @return 影响记录数
     */
    int updateByPk(PK pk, E record);

    /**
     * 修改记录信息
     *
     * @param pk 主键
     * @param record 要修改的对象
     * @return 影响记录数
     */
    int updateByPkSelective(PK pk, E record);

    /**
     * 保存或修改
     *
     * @param record 要修改的数据
     * @return 影响记录数
     */
    PK saveOrUpdate(E record);

}

通用查询接口:

package com.zhuma.demo.comm.service;

import com.zhuma.demo.comm.model.qo.PageQO;
import com.zhuma.demo.comm.model.vo.PageVO;

import java.util.List;


/**
 * @desc 基础查看服务
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
public interface SelectService<E, PK> {

    /**
     * 根据主键查询
     * @param pk 主键
     * @return 查询结果,无结果时返回{@code null}
     */
    E selectByPk(PK pk);

    /**
     * 根据多个主键查询
     * @param pks 主键集合
     * @return 查询结果,如果无结果返回空集合
     */
    List<E> selectByPks(Iterable<PK> pks);

    /**
     * 查询所有结果
     * @return 所有结果,如果无结果则返回空集合
     */
    List<E> selectAll();

    /**
     * 查询所有结果
     * @return 获取分页结果
     */
    PageVO<E> selectPage(PageQO<?> pageQO);

}

通用service的mysql实现:

package com.zhuma.demo.comm.service.impl;

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.zhuma.demo.comm.mapper.CrudMapper;
import com.zhuma.demo.comm.model.po.PO;
import com.zhuma.demo.comm.model.qo.PageQO;
import com.zhuma.demo.comm.model.vo.PageVO;
import com.zhuma.demo.comm.service.CrudService;
import com.zhuma.demo.util.BeanUtil;
import com.zhuma.demo.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;

import lombok.extern.slf4j.Slf4j;
import tk.mybatis.mapper.entity.Condition;
import tk.mybatis.mapper.entity.Example;

/**
 * @desc MYSQL通用CRUD服务类
 * 备注:使用该类时,注入泛型 E,一定要有对应的 EMapper,例如:使用User的基础服务实现类需要继承MySqlCrudServiceImpl<User, String>,
 *       前提是要有UserMapper extends CrudMapper 类
 *
 * @author zhuamer
 * @since 10/18/2017 18:31 PM
 */
@Slf4j
public abstract class MySqlCrudServiceImpl<E extends PO<PK>, PK> implements CrudService<E, PK> {

    @Autowired
    protected CrudMapper<E> crudMapper;

    protected Class<E> poType;

    public MySqlCrudServiceImpl() {
        ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
        poType = (Class<E>) pt.getActualTypeArguments()[0];
    }

    @Override
    public PK insert(E record) {
        Assert.notNull(record, "record is not null");

        if (record.getCreateTime() == null) {
            Date currentDate = new Date();
            record.setCreateTime(currentDate);
            record.setUpdateTime(currentDate);
        }
        crudMapper.insert(record);
        return record.getId();
    }

    @Override
    public int deleteByPk(PK pk) {
        Assert.notNull(pk, "pk is not null");

        return crudMapper.deleteByPrimaryKey(pk);
    }

    @Override
    public int deleteByPks(Iterable<PK> pks) {
        Assert.notNull(pks, "pks is not null");

        String pksStr = this.IterableToSpitStr(pks, ",");
        if (pksStr == null) {
            return 0;
        }

        return crudMapper.deleteByIds(pksStr);
    }

    @Override
    public int updateByPk(PK pk, E record) {
        Assert.notNull(pk, "pk is not null");
        Assert.notNull(record, "record is not null");

        record.setId(pk);
        if (record.getUpdateTime() == null) {
            record.setUpdateTime(new Date());
        }
        return crudMapper.updateByPrimaryKey(record);
    }

    @Override
    public int updateByPkSelective(PK pk, E record) {
        Assert.notNull(pk, "pk is not null");
        Assert.notNull(record, "record is not null");

        record.setId(pk);
        if (record.getUpdateTime() == null) {
            record.setUpdateTime(new Date());
        }
        return crudMapper.updateByPrimaryKeySelective(record);
    }

    @Override
    public PK saveOrUpdate(E record) {
        Assert.notNull(record, "record is not null");

        if (null != record.getId() && null != selectByPk(record.getId())) {
            updateByPk(record.getId(), record);
        } else {
            insert(record);
        }
        return record.getId();
    }

    @Override
    public E selectByPk(PK pk) {
        Assert.notNull(pk, "pk is not null");

        return crudMapper.selectByPrimaryKey(pk);
    }

    @Override
    public List<E> selectByPks(Iterable<PK> pks) {
        Assert.notNull(pks, "pks is not null");

        String pksStr = this.IterableToSpitStr(pks, ",");
        if (pksStr == null) {
            return new ArrayList<>();
        }

        return crudMapper.selectByIds(pksStr);
    }

    private String IterableToSpitStr(Iterable<PK> pks, String separator) {
        StringBuilder s = new StringBuilder();
        pks.forEach(pk -> s.append(pk).append(separator));

        if (StringUtil.isEmpty(s.toString())) {
            return null;
        } else {
            s.deleteCharAt(s.length() - 1);
        }

        return s.toString();
    }

    @Override
    public List<E> selectAll() {
        return crudMapper.selectAll();
    }

    @Override
    public PageVO<E> selectPage(PageQO<?> pageQO) {
        Assert.notNull(pageQO, "pageQO is not null");

        Page<E> page = PageHelper.startPage(pageQO.getPageNum(), pageQO.getPageSize(), pageQO.getOrderBy());
        try {
            Object condition = pageQO.getCondition();
            if (condition == null) {
                crudMapper.selectAll();
            } else if (condition instanceof Condition) {
                crudMapper.selectByCondition(condition);
            } else if (condition instanceof Example) {
                crudMapper.selectByExample(condition);
            } else if (poType.isInstance(condition)){
                crudMapper.select((E)condition);
            } else {
                try {
                    E e = poType.newInstance();
                    BeanUtil.copyProperties(condition, e);
                    crudMapper.select(e);
                } catch (InstantiationException | IllegalAccessException e) {
                    log.error("selectPage occurs error, caused by: ", e);
                    throw new RuntimeException("poType.newInstance occurs InstantiationException or IllegalAccessException", e);
                }
            }
        } finally {
            page.close();
        }

        return PageVO.build(page);
    }

}

备注

  • 该实现类实现了上面的通用服务接口(CrudService),你的业务实现类需继承该类以获得相应的能力。
  • 使用时需注意,使用该类时,注入泛型 E,一定要有对应的 EMapper,例如:使用User的基础服务实现类需要继承MySqlCrudServiceImpl, 前提是要有UserMapper extends CrudMapper 类(这点很重要哈)。
  • 可以看出该实现类也同时维护了createTime、updateTime字段的值,你也就不用管这两个字段的维护了,有的同学可能会有疑问为什么不直接获取mysql的数据库服务上的时间呢,也是很简单的,但是需要注意的是我们的应用服务和mysql数据库服务的时间点通常是不完全一致的(多少会有一些偏差),记录应用服务器的时间也会方便我们后续对线上排查问题时,时间上的一致性。
  • 这里的selectPage方法是我们上一篇文章中给大家留下来的课后思考,从代码中也可以看出PageQO的condition可以接受任意的对象:E、null、Condition、Example或其他的对象(其他对象时会将该对象信息按跟PO类的同名字段拷贝过去进行查询),所以用该selectPage基本上可以满足你对单表分页的所有需求,如果你不太会用通用mapper的Condition和Example类的使用,自行上网上查查还是蛮好用的。
  • 如果不理解PageQO、PageVO可以再看一下这篇文章《通用分页》

成果展示

我们还是以保存用户为例:

用户PO类定义:

package com.zhuma.demo.model.po;

import com.zhuma.demo.annotation.EnumValue;
import com.zhuma.demo.comm.model.po.BasePO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User extends BasePO<String> {

    private static final long serialVersionUID = 1831625735139271430L;

    /**
     * 用户ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "select uuid()")
    @Length(min=1, max=64)
    private String id;

    /**
     * 昵称
     */
    @NotBlank
    @Length(min=1, max=64)
    private String nickname;

    /**
     * 性别
     */
    @NotBlank
    @EnumValue(enumClass=UserGenderEnum.class, enumMethod="isValidName")
    private String gender;

    /**
     * 头像
     */
    @Length(max=256)
    private String avatar;

    /**
     * 状态
     */
    @NotBlank
    @EnumValue(enumClass=UserTypeEnum.class, enumMethod="isValidName")
    private String type;

    /**
     * 账号状态
     */
    @EnumValue(enumClass=UserStatusEnum.class, enumMethod="isValidName")
    private String status;

    /**
     * 用户性别枚举
     */
    public enum UserGenderEnum {
        /**男*/
        MALE,
        /**女*/
        FEMALE,
        /**未知*/
        UNKNOWN;

        public static boolean isValidName(String name) {
            for (UserGenderEnum userGenderEnum : UserGenderEnum.values()) {
                if (userGenderEnum.name().equals(name)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * 用户类型枚举
     */
    public enum UserTypeEnum {
        /**普通*/
        NORMAL,
        /**管理员*/
        ADMIN;

        public static boolean isValidName(String name) {
            for (UserTypeEnum userTypeEnum : UserTypeEnum.values()) {
                if (userTypeEnum.name().equals(name)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * 用户状态枚举
     */
    public enum UserStatusEnum {
        /**启用*/
        ENABLED,
        /**禁用*/
        DISABLED;

        public static boolean isValidName(String name) {
            for (UserStatusEnum userStatusEnum : UserStatusEnum.values()) {
                if (userStatusEnum.name().equals(name)) {
                    return true;
                }
            }
            return false;
        }
    }
}

用户的Mapper类:

package com.zhuma.demo.mapper;

import com.zhuma.demo.comm.mapper.CrudMapper;
import com.zhuma.demo.model.po.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends CrudMapper<User> {
}

用户的Service类定义:

package com.zhuma.demo.service;

import com.zhuma.demo.comm.service.CrudService;
import com.zhuma.demo.model.po.User;

public interface UserService extends CrudService<User, String> {

}

用户的Service类实现:

package com.zhuma.demo.service.impl;

import com.zhuma.demo.comm.service.impl.MySqlCrudServiceImpl;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends MySqlCrudServiceImpl<User, String> implements UserService {

}

备注

  • 这里再次声明,使用UserService的时候一定是已经有了UserMapper extend CrudMapper哈。
package com.zhuma.demo.web.demo2;

import com.zhuma.demo.annotation.ResponseResult;
import com.zhuma.demo.comm.model.qo.PageQO;
import com.zhuma.demo.comm.model.vo.PageVO;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * @desc 用户管理控制器
 * 
 * @author zhumaer
 * @since 1/31/2018 23:57 PM
 */
@ResponseResult
@RestController
@RequestMapping("demo2/users")
public class Demo2UserController {

    @Autowired
    private UserService userService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User addUser(@Validated @RequestBody User user) {
        String userId = userService.insert(user);
        if (userId != null) {
            return userService.selectByPk(userId);
        }
        return null;
    }

    @GetMapping
    public PageVO<User> getPage(PageQO pageQO, User userQO) {
        pageQO.setCondition(userQO);
        return userService.selectPage(pageQO);
    }

}

备注

  • 本实例我们就叫它demo2了,为了方便后续同学们去查看demo实例的代码。
  • 我们仅仅演示了下插入和分页查看功能,注意分页时我们加入了User userQO参数的传递,来测试下分页功能,当然你可以定义自己的任意查询对象。

添加用户PostMan演示截图:

这里写图片描述

分页查看用户列表PostMan演示截图:

这里写图片描述

最后

通用service的第一讲就先到这里啦,不知道对你是否有一些启发呢?下一篇我们讲解《通用树结构》,如果感觉文章还不错或有些疑问可以关注CSDN账号留言或关注下面的公众号加群等方式来联系我O(∩_∩)O~

附上本实例代码github地址:https://github.com/zhumaer/zhuma/tree/master/zhuma-demo

欢迎关注我们的公众号或加群,等你哦!

猜你喜欢

转载自blog.csdn.net/aiyaya_/article/details/79212852
今日推荐