Springboot架構設計(二)封裝
這時候資料庫還沒有準備好,介面需求也沒有定下來,我們可以做一些早期的封裝。早期封裝的好,儘量實現低耦合,就和實現快速開發,而且還能應對各種不確定的變化。
一般的介面需求,以獲取資料為主。獲取資料有些是單一資料型別,有的卻是多種資料多種結構組合在一起。比如Android的頁面如果比較複雜,就需要組裝一套複雜的資料提供。這就導致java後端縱向分割無法確定。
我的觀點是,controller是資料提供層,分割的依據是前端提供的模組,依照前端的一級模組或者介面需求的分法確定名稱空間。service和dao這兩層則是資料處理層,他們是一致的,可以按照資料型別進行劃分,也就是基本依照資料表。資料庫表中聯絡緊密的幾張表可以算作同一種資料型別。
因為介面需求文件還沒有生成,我們不知道controller怎麼處理,所以我們只好先處理單一型別的資料。操作單一型別的資料相當於操作一張表,一般有以下幾種:
1.獲取一條記錄
2.獲取所有的記錄(列表)
3.獲取指定分頁的記錄(封裝在分頁模型中)
4.刪除一條記錄(依據id)
5.刪除多條資料(依據id集合)
6.獲取記錄總數
以上六條是確定的。
以下操作是不確定的。
增加一條記錄:不確定傳進來的引數,除非是用body+json傳進來整個的物件,這樣需要固定請求模式,但是也沒有這樣傳的,浪費流量;
增加多條記錄:同上;
修改一條記錄:同上。
從增刪改查角度分析,可以確定封裝的基本操作有以上六種。我們就按照這六種來進行封裝。
一、首先,自定義一個Repository,實現DAO層的封裝。實際上,JpaRepository已經把這六種操作封裝好了。只是作為程式設計師,要想使自己的程式夠靈活,儘量不要直接用原生的,否則遇到需要修改的時候手忙腳亂。哪怕我們自定義類之後其實什麼都沒有做,只是路過也沒有關係,我們拿到操作權,可以很方便進行維護。
在這裡,我給自己加一個需求。JpaRepository中的分頁操作的資料型別不合我的使用,我要把資料放在自己定義的PageModel中包起來,我決定在DAO層去實現。
1. 首先自定義一個介面,繼承JpaRepository
2. 給上面的介面建立一個實現類,繼承SimpleJpaRepository,實現類的類名就在介面名後面加上“Impl”就可以了。@NoRepositoryBean public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { PageModel<T> getPage(Pageable pageable);//獲取自定義分頁 }
public class BaseRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID>
implements BaseRepository<T, ID> {
private EntityManager entityManager;
public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
/**
* 獲取自定義分頁
*
* @param pageable
* @return
*/
@Override
public PageModel<T> getPage(Pageable pageable) {
PageModel<T> pageModel = new PageModel<T>(pageable.getPageNumber() + 1, pageable.getPageSize());
pageModel.count = count();
pageModel.hasNext = pageModel.page * pageModel.pageSize < pageModel.count;//是否有下一頁
pageModel.dataList = findAll(pageable).getContent();
return pageModel;
}
}
在這個實現類中,我們實現了我們自己新增的方法。
3. 建立處理類,繼承JpaRepositoryFactoryBean
public class BaseRepositoryFactoryBean<JR extends JpaRepository<T, ID>, T, ID extends Serializable>
extends JpaRepositoryFactoryBean<JR, T, ID> {
public BaseRepositoryFactoryBean(Class<? extends JR> repositoryInterface) {
super(repositoryInterface);
}
@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
return new BaseRepositoryFactory(entityManager);
}
private static class BaseRepositoryFactory<T, ID extends Serializable> extends JpaRepositoryFactory {
private final EntityManager entityManager;
public BaseRepositoryFactory(EntityManager entityManager) {
super(entityManager);
this.entityManager = entityManager;
}
@Override
protected Object getTargetRepository(RepositoryInformation information) {
return new BaseRepositoryImpl<T, ID>((Class<T>) information.getDomainType(), entityManager);
}
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return BaseRepositoryImpl.class;
}
}
}
4. 在Application上面加上一句註解,開啟處理工廠
@EnableJpaRepositories(basePackages = "com.meiyue", repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class)
二、自定義Service基類
ublic abstract class BaseService<T, I extends Serializable, R extends BaseRepository<T, I>> {
@Autowired
R dao;
//1. 查詢:一條資料
public T getOne(I id) {
return dao.findOne(id);
}
//2. 查詢:資料列表 按照id升序排列
public List<T> getList() {
return dao.findAll(new Sort(Sort.Direction.ASC, "id"));
}
//3. 查詢:資料分頁 按照id升序排列
public PageModel<T> getPage(int page, int pageSize) {
//資料庫分頁查詢起始id是從0開始的,請求的頁碼是從1開始的,所以處理的時候要減一
return page > 0 ? dao.getPage(new PageRequest(page - 1, pageSize, new Sort(Sort.Direction.ASC, "id"))) : null;
}
//4. 增加:增加一條資料
public boolean addOne(T t) {
T data = dao.save(t);
return data != null ? true : false;
}
//5. 增加:增添批量資料
public boolean addList(List<T> dataList) {
//todo 此處要做事務處理
try {
for (T t : dataList) {
addOne(t);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//6. 刪除:刪除一條記錄
public boolean removeOne(I id) {
try {
dao.delete(id);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//7. 刪除:批量刪除
public boolean removeList(String ids) {
String[] idss = ids.split(",");
//todo 此處需要事務處理
try {
for (String id : idss) {
removeOne((I) id);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//8. 修改:修改一條記錄 物件必須包含id
public boolean updateOne(T t) {
try {
dao.save(t);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
在這裡封裝了增加和修改的操作,是方便子類的呼叫。在子類將物件構建好之後,就可以直接呼叫,也可以呼叫DAO進行操作,由於職司不同不建議在controller中構建物件進行呼叫,雖然這也可以。
三、自定義controller抽象類。嚴格地說,這個封裝的用處不大,因為介面並不是按照資料型別分得這麼清楚。不過也不排除有些介面就是操作某一種型別單一的資料,那就可以用上了,而且,可以快速實現。
由於封裝要考慮KV請求和body請求兩種方式,所封裝的方法引數結構是不一樣的,所以要分開封裝,在這裡只貼出KV請求的封裝,body請求原理相同。
public abstract class BaseKvController<T, I extends Serializable, S extends BaseService> {
public abstract S getService();//獲得Service 處理不好自動裝載的笨辦法
//1.獲取一個數據
@RequestMapping("/getOne")
public NetResult<T> getOne(I id) {
return ResultUtils.buildResult((T)(getService().getOne(id)));
}
//2.獲取資料列表
@RequestMapping("/getList")
public NetResult<List<T>> getList() {
return ResultUtils.buildResult(getService().getList());
}
//3.獲取分頁
@RequestMapping("/getPage")
public NetResult<PageModel<T>> getPage(int page, int pageSize) {
return ResultUtils.buildResult(getService().getPage(page, pageSize));
}
//4.刪除一條
@RequestMapping("/removeOne")
public NetResult<Boolean> removeOne(I id) {
return ResultUtils.buildResult(getService().removeOne(id));
}
//5.刪除一批
@RequestMapping("/removeList")
public NetResult<Boolean> removeList(String ids) {
return ResultUtils.buildResult(getService().removeList(ids));
}
}
可以看到這裡有5中常見操作。
四、還有幾個工具類,也一併貼出來
1.構造結果的工具類ResultUtils(見上面)
2.構造json的工具類(簡易版)
public class JsonUtils {
private static Gson gson = new Gson();
public static String toJson(Object obj) {
return gson.toJson(obj);
}
}
3.列印後臺日誌的工具類
public class MsgUtils {
private static Logger logger = LoggerFactory.getLogger(MsgUtils.class);
public static void println(Object obj) {
System.out.println(obj);
}
public static void print(Object obj) {
System.out.print(obj);
}
public static void d(Object obj) {
logger.debug(String.valueOf(obj));
}
public static void i(Object obj) {
logger.info(String.valueOf(obj));
}
public static void w(Object obj) {
logger.warn(String.valueOf(obj));
}
public static void e(Object obj) {
logger.error(String.valueOf(obj));
}
}
我們來實踐一下:
首先連線資料庫
在專案上右鍵點選,選擇Add Framework Support,選中JavaEE persistence,選中hibernate,下載確定。
這是Idea左下角會有persistence視窗,在專案上點選右鍵,選中Gennarate Persistence Mapping->By Database schema,選擇對應的表和引數,生成自帶註解的實體類,每個實體類對應一張表。
我們選擇其中一個TestCitiesEntity作為我們測試的資料型別來操作。
1.建立DAO
public interface CityDao extends BaseRepository<TestCitiesEntity, Integer> {
}
可以看到,一句程式碼都沒有,就是繼承了我們封裝的介面。
2.建立Service
@Service
public class CityService extends BaseService<TestCitiesEntity, Integer, CityDao> {
}
同樣是一句程式碼都沒有,註解要加上。
3.建立controller
@RestController
@RequestMapping("/city")
public class CityController extends BaseKvController<TestCitiesEntity, Integer, CityService>{
@Autowired
CityService cityService;
@Override
public CityService getService() {
return cityService;
}
}
這個我目前沒解決BaseService的自動裝配問題,所以留出了一個抽象方法需要實現。只加了簡單的一點程式碼。上面的兩行註解是必須的。
我們啟動測試一下。資料庫有可查詢的資料。
我們在PostMan裡請求192.168.1.101:8080/yuedao/city/getPage?page=2&pageSize=4
看看結果:
我們一個介面都沒寫,但是我們已經有介面可以用了。
換做那些複雜資料介面,我們也只需要把各種需要的service注入進去,進行廣泛的呼叫組裝就可以。至於那些封裝顧及不到的,特事特辦,已經很少了。
修改一個地方,就是controller的封裝實際上是可以封裝新增資料和修改一條資料的,我剛瞭解到,controller可以接收一個物件,而在請求的時候只需要提供相同的欄位就可以了。不過,修改呼叫是必須要提供id的。
把下面這部分加到BaseKvController裡面:
//6.新增一條資料
@RequestMapping("/addOne")
public NetResult<Boolean> addOne(T t) {
return ResultUtils.buildResult(getService().addOne(t));
}
//6.修改一條資料
@RequestMapping("/updateOne")
public NetResult<Boolean> updateOne(T t) {
return ResultUtils.buildResult(getService().updateOne(t));
}
測試沒有問題,新增資料和修改資料都有效。不過由於無法判斷泛型是否包含id所以無法對修改資料進行驗證。如果不傳id,就會增加一條資料,只有引數中包含一個可用的id,才會成功修改。