新技術?
spring-boot並不是全新的技術棧,而是整合了spring的很多元件,並且以約定優先的原則進行組合。使用boot我們不需要對冗雜的配置檔案進行管理,主需要用它的註解便可啟用大部分web開發中所需要的功能。本篇就是基於boot來配置jpa和靜態檔案訪問,進行web應用的開發。
模板or靜態頁面
最原始的jsp頁面在springboot中已經不在預設支援,spring-boot預設使用thymeleaf最為模板。當然我們也可以使用freemark或者velocity等其他後端模板。但是按照前後端分離的良好設計,我們最好採用靜態頁面作為前端模板,這樣前後端完全分離,把資料處理邏輯寫在程式並提供介面供前端呼叫。這樣的設計更加靈活和清晰。
專案搭建
我們將討論專案的結構、application配置檔案、靜態頁面處理、自定義filter,listener,servlet以及攔截器的使用。最後集中討論jpa的配置和操作以及如何進行單元測試和打包部署。
專案結構
專案使用maven進行依賴管理和構建,整體結構如下圖所示:
我們的HTML頁面和資原始檔都在resources/static下,打成jar包的時候static目錄位於/BOOT-INF/classes/。
pom.xml
我們需要依賴下面這些包:
|
|
spring-boot-starter-parent使我們專案的父pom。
spring-boot-starter-web提供嵌入式tomcat容器,從而使專案可以通過打成jar包的方式直接執行。
spring-boot-starter-data-jpa引入了jpa的支援。
spring-boot-test和junit配合做單元測試。
mysql-connector-java和HikariCP做資料庫的連線池的操作。
spring-boot-maven-plugin外掛能把專案和依賴的jar包以及資原始檔和頁面一起打成一個可執行的jar(執行在內嵌的tomcat)
啟動人口
|
|
這裡ServletComponentScan註解是啟用servlet3的servler和filter以及listener的支援,下面會提到該用法。要注意的是:不能引入@EnableWebMvc註解,否則需要重新配置檢視和資原始檔對映。這樣就不符合我們的前後端分離的初衷了。
靜態資源處理
spring-boot預設會去classpath下面的/static/,/public/ ,/resources/目錄去讀取靜態資源。因此按照約定優先的原則,我們直接把我們應用的頁面和資原始檔直接放在/static下面,如下圖所示:
這樣我們訪問系統主頁就會自動載入index.html,而且它所引用的資原始檔也會在static/下開始載入。
application.yml
我們在application配置檔案中設定各種引數,它可以是傳統的properties檔案也可以使用yml來逐級配置。本文采用的第二種方式yml,如果不懂可以參考:baike.baidu.com/item/YAML/1…。其內容如下:
|
|
使用idea開發工具在編輯器會有自動變數提示,這樣非常方便進行引數的選擇和查閱。
server
server節點可以配置容器的很多引數,比如:埠,訪問路徑、還有tomcat本身的一些引數。這裡設定了session的超時以及編碼等。
logging
日誌級別可以定義到具體的哪個包路徑,日誌檔案的配置要注意:path和file配置一個就行,file預設會在程式工作目錄下生成,也可以置頂絕對路徑進行指定。
datasource
這裡使用號稱效能最牛逼的連線池hikaricp,具體配置可以參閱其官網:brettwooldridge.github.io/HikariCP/
jpa
這裡主要注意下strategy的配置,關係到自動建表時的欄位命名規則。預設會生成帶_劃線分割entity的欄位名(駱駝峰式)。
|
|
註冊web元件
1) 最新的spring-boot引入新的註解ServletComponentScan,使用它可以方便的配置Servlet3+的web元件。主要有下面這三個註解:
|
|
只要把這些註解標記元件即可完成註冊。
|
|
|
|
2) 攔截器的使用
攔截器不是Servlet規範的標準元件,它跟上面的三個元件不在一個處理鏈上。攔截器是spring使用AOP實現的,對controller執行前後可以進行干預,直接結束請求處理。而且攔截器只能對流經dispatcherServlet處理的請求才生效,靜態資源就不會被攔截。
下面頂一個攔截器:
|
|
要想它生效則需要加入攔截器棧:
|
|
jpa操作
spring-boot已經整合了JPA的Repository封裝,基於註解的事務處理等,我們只要按照常規的JPA使用方法即可。以Node表的操作為例:
- 定義entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 複製程式碼
package gxf.dev.topology.entity; import com.fasterxml.jackson.annotation.JsonInclude; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; /** * Created by gongxufan on 2014/11/20. */ @Entity @Table(name = "node") @JsonInclude(JsonInclude.Include.NON_NULL) public class Node implements Serializable { @Id private String id; private String elementType; private String x; private String y; private String width; private String height; private String alpha; private String rotate; private String scaleX; private String scaleY; private String strokeColor; private String fillColor; private String shadowColor; private String shadowOffsetX; private String shadowOffsetY; private String zIndex; private String text; private String font; private String fontColor; private String textPosition; private String textOffsetX; private String textOffsetY; private String borderRadius; private String deviceId; private String dataType; private String borderColor; private String offsetGap; private String childNodes; private String nodeImage; private String templateId; private String deviceA; private String deviceZ; private String lineType; private String direction; private String vmInstanceId; private String displayName; private String vmid; private String topoLevel; private String parentLevel; private Setring nextLevel; //getter&setter } 複製程式碼
JsonInclude註解用於返回JOSN字串是忽略為空的欄位。
編寫repository介面
1 2 3 4 5 6 7 複製程式碼
package gxf.dev.topology.repository; import gxf.dev.topology.entity.Node; import org.springframework.data.repository.PagingAndSortingRepository; public interface NodeRepository extends PagingAndSortingRepository<Node, String> { } 複製程式碼
編寫Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 複製程式碼
package gxf.dev.topology.service; import gxf.dev.topology.entity.Node; import gxf.dev.topology.repository.NodeRepository; import gxf.dev.topology.repository.SceneRepository; import gxf.dev.topology.repository.StageRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; /** * dao操作 * author:gongxufan * date:11/13/17 **/ @Component public class TopologyService { @Autowired private NodeRepository nodeRepository; @Autowired private SceneRepository sceneRepository; @Autowired private StageRepository stageRepository; @Transactional public Node saveNode(Node node) { return nodeRepository.save(node); } public Iterable<Node> getAll() { return nodeRepository.findAll(); } } 複製程式碼
單元測試
單元測試使用spring-boot-test和junit進行,需要用到下面的幾個註解:
|
|
測試程式碼如下:
|
|
jpa補充
使用JPA進行單表操作確實很方便,但是對於多表連線的複雜查詢可能不太方便。一般有兩種方式彌補這個不足:
- 一個是在Query裡標註為NativeQuery,直接使用原生SQL。不過這種方式在動態引數查詢到額情況下很不方便,這時候我們需要按條件拼接SQL。
- 自定義DAO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 複製程式碼
package gxf.dev.topology.repository; import com.mysql.jdbc.StringUtils; import org.hibernate.SQLQuery; import org.hibernate.criterion.CriteriaSpecification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Query; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 支援自定義SQL查詢 * Created by gongxufan on 2016/3/17. */ @Component public class CustomSqlDao { @Autowired private EntityManagerFactory entityManagerFactory; public int getMaxColumn(final String filedName, final String tableName) { String sql = "select nvl(max(" + filedName + "), 0) as max_num from " + tableName; Map map = entityManagerFactory.getProperties(); String dialect = (String) map.get("hibernate.dialect"); //determine which database use if(!StringUtils.isNullOrEmpty(dialect)){ if(dialect.contains("MySQL")){ sql = "select ifnull(max(" + filedName + "), 0) as max_num from " + tableName; } if(dialect.contains("Oracle")){ sql = "select nvl(max(" + filedName + "), 0) as max_num from " + tableName; } } int maxID = 0; List<Map<String, Object>> list = this.querySqlObjects(sql); if (list.size() > 0) { Object maxNum = list.get(0).get("max_num"); if(maxNum instanceof Number) maxID = ((Number)maxNum).intValue(); if(maxNum instanceof String) maxID = Integer.valueOf((String)maxNum); } return maxID + 1; } public List<Map<String, Object>> querySqlObjects(String sql, Integer currentPage, Integer rowsInPage) { return this.querySqlObjects(sql, null, currentPage, rowsInPage); } public List<Map<String, Object>> querySqlObjects(String sql) { return this.querySqlObjects(sql, null, null, null); } public List<Map<String, Object>> querySqlObjects(String sql, Map params) { return this.querySqlObjects(sql, params, null, null); } @SuppressWarnings("unchecked") public List<Map<String, Object>> querySqlObjects(String sql, Object params, Integer currentPage, Integer rowsInPage) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query qry = entityManager.createNativeQuery(sql); SQLQuery s = qry.unwrap(SQLQuery.class); //設定引數 if (params != null) { if (params instanceof List) { List<Object> paramList = (List<Object>) params; for (int i = 0, size = paramList.size(); i < size; i++) { qry.setParameter(i + 1, paramList.get(i)); } } else if (params instanceof Map) { Map<String, Object> paramMap = (Map<String, Object>) params; Object o = null; for (String key : paramMap.keySet()) { o = paramMap.get(key); if (o != null) qry.setParameter(key, o); } } } if (currentPage != null && rowsInPage != null) {//判斷是否有分頁 // 起始物件位置 qry.setFirstResult(rowsInPage * (currentPage - 1)); // 查詢物件個數 qry.setMaxResults(rowsInPage); } s.setResultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP); List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>(); try { List list = qry.getResultList(); resultList = s.list(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return resultList; } public int getCount(String sql) { String sqlCount = "select count(0) as count_num from " + sql; List<Map<String, Object>> list = this.querySqlObjects(sqlCount); if (list.size() > 0) { int countNum = ((BigDecimal) list.get(0).get("COUNT_NUM")).intValue(); return countNum; } else { return 0; } } /** * 處理sql語句 * * @param _strSql * @return */ public String toSql(String _strSql) { String strNewSql = _strSql; if (strNewSql != null) { strNewSql = regReplace("'", "''", strNewSql); } else { strNewSql = ""; } return strNewSql; } private String regReplace(String strFind, String strReplacement, String strOld) { String strNew = strOld; Pattern p = null; Matcher m = null; try { p = Pattern.compile(strFind); m = p.matcher(strOld); strNew = m.replaceAll(strReplacement); } catch (Exception e) { } return strNew; } /** * 根據hql語句查詢資料 * * @param hql * @return */ @SuppressWarnings("rawtypes") public List queryForList(String hql, List<Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); List list = null; try { if (params != null && !params.isEmpty()) { for (int i = 0, size = params.size(); i < size; i++) { query.setParameter(i + 1, params.get(i)); } } list = query.getResultList(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return list; } @SuppressWarnings("rawtypes") public List queryByMapParams(String hql, Map<String, Object> params, Integer currentPage, Integer pageSize) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); List list = null; try { if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } if (currentPage != null && pageSize != null) { query.setFirstResult((currentPage - 1) * pageSize); query.setMaxResults(pageSize); } list = query.getResultList(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return list; } @SuppressWarnings("rawtypes") public List queryByMapParams(String hql, Map<String, Object> params) { return queryByMapParams(hql, params, null, null); } @SuppressWarnings("rawtypes") public List queryForList(String hql) { return queryForList(hql, null); } /** * 查詢總數 * * @param hql * @param params * @return */ public Long queryCount(String hql, Map<String, Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); Long count = null; try { if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } count = (Long) query.getSingleResult(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return count; } /** * 查詢總數 * * @param sql * @param params * @return */ public Integer queryCountBySql(String sql, Map<String, Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Integer count = null; try { Query query = entityManager.createNativeQuery(sql); if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } Object obj = query.getSingleResult(); if (obj instanceof BigDecimal) { count = ((BigDecimal) obj).intValue(); } else { count = (Integer) obj; } } finally { if (entityManager != null) { entityManager.close(); } } return count; } /** * select count(*) from table * * @param sql * @param params * @return */ public int executeSql(String sql, List<Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); try { Query query = entityManager.createNativeQuery(sql); if (params != null && !params.isEmpty()) { for (int i = 0, size = params.size(); i < size; i++) { query.setParameter(i + 1, params.get(i)); } } return query.executeUpdate(); } finally { if (entityManager != null) { entityManager.close(); } } } } 複製程式碼
我們在service層注入,然後就可以根據輸入條件拼接好sql或者hql來進行各種操作。這種方式靈活而且也不需要手動寫分頁程式碼,使用hibernate封裝好的機制即可。
總結
使用boot可以快速搭建一個前後端開發的骨架,裡面有很多自動的配置和約定。雖然boot不是新的一個技術棧,但是它要求我們對各個元件都要比較熟悉,不然對它的執行機制和約定配置會感到很困惑。而使用JPA進行資料庫操作也是利弊參半,需要自己權衡。
專案程式碼:github.com/gongxufan/t…