1. 程式人生 > >Spring Boot專案通用功能第一講之《通用Service》

Spring Boot專案通用功能第一講之《通用Service》

前言

上兩篇文章中我們說了下怎麼去做《通用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~

歡迎關注我們的公眾號或加群,等你哦!