1. 程式人生 > >一個基於Spring Boot的API、RESTful API專案種子(骨架)

一個基於Spring Boot的API、RESTful API專案種子(骨架)

前言

最近使用Spring Boot 配合 MyBatis 、通用Mapper外掛、PageHelper分頁外掛 連做了幾個中小型API專案,做下來覺得這套框架、工具搭配起來開發這種專案確實非常舒服,團隊的反響也不錯。在專案搭建和開發的過程中也總結了一些小經驗,與大家分享一下。

在開發一個API專案之前,搭建專案、引入依賴、配置框架這些基礎活自然不用多說,通常為了加快專案的開發進度(早點回家)還需要封裝一些常用的類和工具,比如統一的響應結果封裝、統一的異常處理、介面簽名認證、基礎的增刪改差方法封裝、基礎程式碼生成工具等等,有了這些專案才能開工。

然而,下次再做類似的專案上述那些步驟可能還要搞一遍,雖然通常是拿過來改改,但是還是比較浪費時間。所以,可以利用面向物件抽象、封裝的思想,抽取這類專案的共同之處封裝成了一個種子專案(估計大部分公司都會有很多類似的種子專案),這樣的話下次再開發類似的專案直接在該種子專案上迭代就可以了,減少無意義的重複工作。

在相關專案上線之後,我花了點時間對該種子專案做了一些精簡,並且已經把該專案分享到GitHub上面了,如果你正準備做類似專案的話,可以去克隆下來試試,專案地址&使用文件:github.com/lihengming/… 。如果在使用中發現問題或者有什麼好建議的話歡迎提issue或pr一起來完善它。

特徵&提供

  • 最佳實踐的專案結構、配置檔案、精簡的POM

注:使用程式碼生成器生成程式碼後會建立model、dao、service、web等包。

  • 統一響應結果封裝及生成工具
/**
* 統一API響應結果封裝
*/
public class
Result { private int code; private String message; private Object data; public Result setCode(ResultCode resultCode) { this.code = resultCode.code; return this; } //省略getter、setter方法 } /** * 響應碼列舉,參考HTTP狀態碼的語義 */ public enum ResultCode { SUCCESS(200),//成功 FAIL(400),//失敗 UNAUTHORIZED
(401),//未認證(簽名錯誤) NOT_FOUND(404),//介面不存在 INTERNAL_SERVER_ERROR(500);//伺服器內部錯誤 public int code; ResultCode(int code) { this.code = code; } } /** * 響應結果生成工具 */ public class ResultGenerator { private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS"; public static Result genSuccessResult() { return new Result() .setCode(ResultCode.SUCCESS) .setMessage(DEFAULT_SUCCESS_MESSAGE); } public static Result genSuccessResult(Object data) { return new Result() .setCode(ResultCode.SUCCESS) .setMessage(DEFAULT_SUCCESS_MESSAGE) .setData(data); } public static Result genFailResult(String message) { return new Result() .setCode(ResultCode.FAIL) .setMessage(message); } }
  • 統一異常處理
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    exceptionResolvers.add(new HandlerExceptionResolver() {
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
            Result result = new Result();
            if (e instanceof ServiceException) {//業務失敗的異常,如“賬號或密碼錯誤”
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
                logger.info(e.getMessage());
            } else if (e instanceof NoHandlerFoundException) {
                result.setCode(ResultCode.NOT_FOUND).setMessage("介面 [" + request.getRequestURI() + "] 不存在");
            } else if (e instanceof ServletException) {
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
            } else {
                result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage("介面 [" + request.getRequestURI() + "] 內部錯誤,請聯絡管理員");
                String message;
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    message = String.format("介面 [%s] 出現異常,方法:%s.%s,異常摘要:%s",
                            request.getRequestURI(),
                            handlerMethod.getBean().getClass().getName(),
                            handlerMethod.getMethod().getName(),
                            e.getMessage());
                } else {
                    message = e.getMessage();
                }
                logger.error(message, e);
            }
            responseResult(response, result);
            return new ModelAndView();
        }

    });
}
  • 常用基礎方法抽象封裝
public interface Service<T> {
  void save(T model);//持久化
  void save(List<T> models);//批量持久化
  void deleteById(Integer id);//通過主鍵刪除
  void deleteByIds(String ids);//批量刪除 eg:ids -> “1,2,3,4”
  void update(T model);//更新
  T findById(Integer id);//通過ID查詢
  T findBy(String fieldName, Object value) throws TooManyResultsException; //通過Model中某個成員變數名稱(非資料表中column的名稱)查詢,value需符合unique約束
  List<T> findByIds(String ids);//通過多個ID查詢//eg:ids -> “1,2,3,4”
  List<T> findByCondition(Condition condition);//根據條件查詢
  List<T> findAll();//獲取所有
}
  • 提供程式碼生成器來生成基礎程式碼
public abstract class CodeGenerator {
  ...
  public static void main(String[] args) {
      genCode("輸入表名");
  }
  public static void genCode(String... tableNames) {
      for (String tableName : tableNames) {
          //根據需求生成,不需要的注掉,模板有問題的話可以自己修改。
          genModelAndMapper(tableName);
          genService(tableName);
          genController(tableName);
      }
  }
...
}

CodeGenerator 可根據表名生成對應的Model、Mapper、MapperXML、Service、ServiceImpl、Controller(預設提供POST和RESTful兩套Controller模板,根據需要在 genController(tableName)方法中自己選擇,預設是純POST的),程式碼模板可根據實際專案的需求來定製,以便漸少重複勞動。由於每個公司業務都不太一樣,所以只提供了一些簡單的通用方法模板,主要是提供一個思路來減少重複程式碼的編寫。在我們公司的實際使用中,其實根據業務的抽象編寫了大量的程式碼模板。

  • 提供簡單的介面簽名認證
public void addInterceptors(InterceptorRegistry registry) {
  //介面簽名認證攔截器,該簽名認證比較簡單,實際專案中可以使用Json Web Token或其他更好的方式替代。
  if (!"dev".equals(env)) { //開發環境忽略簽名認證
      registry.addInterceptor(new HandlerInterceptorAdapter() {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //驗證簽名
              boolean pass = validateSign(request);
              if (pass) {
                  return true;
              } else {
                  logger.warn("簽名認證失敗,請求介面:{},請求IP:{},請求引數:{}",
                          request.getRequestURI(), getIpAddress(request), JSON.toJSONString(request.getParameterMap()));

                  Result result = new Result();
                  result.setCode(ResultCode.UNAUTHORIZED).setMessage("簽名認證失敗");
                  responseResult(response, result);
                  return false;
              }
          }
      });
  }
}

/**
* 一個簡單的簽名認證,規則:
* 1. 將請求引數按ascii碼排序
* 2. 拼接為a=value&b=value...這樣的字串(不包含sign)
* 3. 混合金鑰(secret)進行md5獲得簽名,與請求的簽名進行比較
*/
private boolean validateSign(HttpServletRequest request) {
      String requestSign = request.getParameter("sign");//獲得請求籤名,如sign=19e907700db7ad91318424a97c54ed57
      if (StringUtils.isEmpty(requestSign)) {
          return false;
      }
      List<String> keys = new ArrayList<String>(request.getParameterMap().keySet());
      keys.remove("sign");//排除sign引數
      Collections.sort(keys);//排序

      StringBuilder sb = new StringBuilder();
      for (String key : keys) {
          sb.append(key).append("=").append(request.getParameter(key)).append("&");//拼接字串
      }
      String linkString = sb.toString();
      linkString = StringUtils.substring(linkString, 0, linkString.length() - 1);//去除最後一個'&'

      String secret = "Potato";//金鑰,自己修改
      String sign = DigestUtils.md5Hex(linkString + secret);//混合金鑰md5

      return StringUtils.equals(sign, requestSign);//比較
}
  • 整合MyBatis、通用Mapper外掛、PageHelper分頁外掛,實現單表業務零SQL

  • 使用Druid Spring Boot Starter 整合Druid資料庫連線池與監控

  • 使用FastJsonHttpMessageConverter,提高JSON序列化速度

技術選型&文件


後言

感謝大家的支援,沒想到一個簡單的專案總結分享在短短兩天獲得這麼多人的關注,還上了GitHub Trending榜單,有點受寵若驚哈,話說現在國內技術社群氛圍真是越來越好了,希望大家有時間的話都能參與到開源分享的行列來,分享知識,快樂編碼,共勉。

個人部落格

更多前端技術文章美術設計wordpress外掛、優化教程學習筆記盡在我的個人部落格喵容 - 和你一起描繪生活,歡迎一起交流學習,一起進步:http://panmiaorong.top

站內文章推薦:

「前端進階」史上最全的前端學習路線

「不要重複造輪子系列」 前端常用外掛、工具類庫彙總

如何保障前端專案程式碼質量

記錄一次基於vue、typescript、pwa的專案由開發到部署

小程式挖坑之路

原文連結:一個基於Spring Boot的API、RESTful API專案種子(骨架)