JAVA WEB快速入門系列以前的相關文章以下:(文章所有本人【夢在旅途原創】,文中內容可能部份圖片、代碼參照網上資源)javascript
第一篇:JAVA WEB快速入門之環境搭建css
第二篇:JAVA WEB快速入門之從編寫一個JSP WEB網站了解JSP WEB網站的基本結構、調試、部署html
第三篇:JAVA WEB快速入門之經過一個簡單的Spring項目瞭解Spring的核心(AOP、IOC)前端
第四篇:JAVA WEB快速入門之從編寫一個基於SpringMVC框架的網站了解Maven、SpringMVC、SpringJDBCvue
今天是第五篇,也是該系列文章的最後一篇,接上篇《JAVA WEB快速入門之從編寫一個基於SpringMVC框架的網站了解Maven、SpringMVC、SpringJDBC》,經過上篇文章的詳細介紹,知道如何使用maven來快速構建spring MVC應用,也可以使用spring MVC+springJDBC實現網站開發,而本文所涉及的知識則是在這基礎之上繼續提高,核心是講解如何使用spring boot來更快速的構建spring MVC,並經過mybatis及代碼生成相關DAO,同時利用VUE前端框架開發先後端分離的網站,用戶體驗更好,廢話很少說,直接進入本文主題。java
(提示:本文內容有點長,涉及的知識點也比較多,如果新手建議耐心看完!)git
1、建立Spring Boot+SpringMVC空項目github
1.1經過https://start.spring.io/官網快速生成一個Spring Boot+SpringMVC空項目,以下圖示:web
(固然也能夠經過Eclipse或IDEA的Spring Boot插件來建立,可參見:http://www.javashuo.com/article/p-uwkbhixv-bz.html,https://blog.csdn.net/qq_32572497/article/details/62037873)spring
設置後點擊頁面的生成項目按鈕,便可生成並下載spring boot項目代碼壓縮包,而後使用IDE導入存在的maven project便可。
1.2調整項目,解決一些踩坑點
1.2.1.調整spring boot App啓動類(如:SpringbootdemoApplication)到根包目錄或在啓動類上顯式添加@ComponentScan註解,並指定包路徑,以下代碼所示,cn.zuowenjun.boot是根包目錄,其他都是cn.zuowenjun.boot的子包
package cn.zuowenjun.boot; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.transaction.annotation.EnableTransactionManagement; //import org.springframework.context.annotation.ComponentScan; @SpringBootApplication //指定爲Spring Boot啓動入口,內含多個spring所須要的註解 @MapperScan(basePackages="cn.zuowenjun.boot.mapper")//設置Mybaits掃描的mapper包路徑 //@ComponentScan(basePackages= {"cn.zuowenjun.controller"}) //若是不在根包目錄,則需指定spring管理的相關包路徑 @EnableTransactionManagement //啓動事務管理 public class SpringbootdemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootdemoApplication.class, args); } }
1.2.2.解決POM文件報:
Description Resource Path Location Type
Execution default-resources of goal org.apache.maven.plugins:maven-resources-plugin:3.1.0:resources failed: Unable to load the mojo 'resources' (or one of its required components) from the plugin 'org.apache.maven.plugins:maven-resources-plugin:3.1.0'
直接在POM中添加以下resources依賴:
<dependency> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <type>maven-plugin</type> </dependency>
1.2.3.設置熱編譯啓動模式,以即可以隨時更改代碼後即時生效
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> </plugins> </build>
設置後項目的視圖就有以下顯示效果:
1.3演示請求REST API分別返回JSON、XML
建立好spring boot空項目環境後,咱們就能夠開始編寫相關代碼了,在此僅貼出實現了REST API分別響應返回JSON、XML格式的Controller,實現步驟以下:
1.3.1在cn.zuowenjun.boot.controller包中建立DemoController,並編寫hellojson、helloxml Action方法,代碼以下:
package cn.zuowenjun.boot.controller; import org.springframework.web.bind.annotation.*; import cn.zuowenjun.boot.domain.*; @RestController public class DemoController { @RequestMapping(value="/hello/json",produces="application/json;charset=utf-8") public HelloDto hellojson() { HelloDto dto=new HelloDto(); dto.setMessage("hello,zuowenjun.cn,hello java spring boot!"); return dto; } @RequestMapping(value="/hello/xml",produces="text/xml;charset=utf-8") public HelloDto helloxml() { HelloDto dto=new HelloDto(); dto.setMessage("hello,zuowenjun.cn,hello java spring boot!"); return dto; } }
如上代碼簡要說明:@RestController至關因而:@Controller、@ResponseBody,這個能夠查看@RestController註解類代碼就知道;@RequestMapping指定請求映射,其中produces設置響應內容格式(可理解爲服務端是生產者,而用戶在瀏覽器端【客戶端】是消費端),還有consumes屬性,這個是指可接收請求的內容格式(可理解爲用戶在瀏覽器端發送請求是消息的生產者,而服務端接收並處理該請求爲消息的消費者),固然還有其它一些屬性,你們能夠參見我上篇文章或網絡其它大神的相關文章加以瞭解。
另外須要注意,默認spring MVC只返回JSON格式,若需返回XML格式,還需添加XML JAR包依賴,以下:(能夠看到version這裏我指定了版本號區間,表示2.5.0及以上版本均可以,有些依賴spring-boot-starter-parent中都有提早配置依賴管理,咱們只須要指定groupId、artifactId便可,version就會使用spring boot中的默認版本,固然也能夠強制指定版本)
<!-- 若是項目中須要REST API響應(返回)XML格式的報文體則應添加該依賴 --> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-xml-provider</artifactId> <version>[2.5.0,)</version><!--$NO-MVN-MAN-VER$ --> </dependency>
因爲項目中同時添加JSON及XML的JAR包,按照spring MVC的默認響應處理流程是:若是未指定produces,則當請求的header中指定了accept類型,則自動格式化並返回該accept所需的類型,若是未指定accept類型,則優先是響應XML,當找不到XML依賴包時纔會響應JSON,故若是項目中同時有JSON及XML,那麼最好顯式指定produces或者請求頭上指明accept類型 這一點與ASP.NET WEB API原理相同,由於都是符合REST架構風格的。
效果以下:
2、使用Mybatis框架完成Domain層、DAO層(這裏是Mapper層) ---提示:因爲篇幅有限,只貼出重點能體現不一樣知識點的代碼,其他能夠到GITHUB上查看下載源碼進行詳細瞭解
2.0:首先在application.properties配置mybatis的相關選項,以下所示:
mybatis.type-aliases-package=cn.zuowenjun.boot.domain #包類型別名,這樣在XML中就能夠簡寫成類名
mybatis.config-location=classpath:mybatis/mybatis-config.xml #指定mybatis的配置文件路徑
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml #指定mapper XML的存放路徑
#這裏是使用SQL SERVER,若是是其它DB則使用其它驅動
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.url=jdbc:sqlserver://DBIP:Port;DatabaseName=testDB
spring.datasource.username=dbuser
spring.datasource.password=dbpassword
其次添加mybatis-spring-boot-starter maven依賴,它會自動添加相關的mybatis依賴包,配置以下:
<!-- 添加 mybatis-spring-boot依賴,直接可使用mybatis環境操做DB--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>
2.1全手寫JAVA代碼實現Mybatis的CRUD;
2.1.1.在cn.zuowenjun.boot.domain包【實體模型或稱領域模型層,這裏算不上真正的領域模型,最多算是貧血的領域模型】中定義數據實體模型(Goods:商品信息),代碼以下:
package cn.zuowenjun.boot.domain; import java.math.BigDecimal; import java.util.Date; public class Goods { private int id; private String title; private String picture; private BigDecimal price; private String introduction; private int categoryId; private String lastEditBy; private Date lastEditTime; public Goods() { } public Goods(int id,String title,String picture, BigDecimal price,String introduction,int categoryId,String lastEditBy,Date lastEditTime) { this.setId(id); this.setTitle(title); this.setPicture(picture); this.setPrice(price); this.setIntroduction(introduction); this.setCategoryId(categoryId); this.setLastEditBy(lastEditBy); this.setLastEditTime(lastEditTime); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getPicture() { return picture; } public void setPicture(String picture) { this.picture = picture; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public String getIntroduction() { return introduction; } public void setIntroduction(String introduction) { this.introduction = introduction; } public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; } public String getLastEditBy() { return lastEditBy; } public void setLastEditBy(String lastEditBy) { this.lastEditBy = lastEditBy; } public Date getLastEditTime() { return lastEditTime; } public void setLastEditTime(Date lastEditTime) { this.lastEditTime = lastEditTime; } }
2.1.2.在cn.zuowenjun.boot.mapper包【數據映射處理層或稱DAO層】中定義數據映射處理接口及添加相應的SQL註解,以實現對數據進行CRUD,代碼以下:
package cn.zuowenjun.boot.mapper; import java.util.*; import org.apache.ibatis.annotations.*; import cn.zuowenjun.boot.domain.*; public interface GoodsMapper { @Select("select * from TA_TestGoods order by id offset (${pageNo}-1)*${pageSize} rows fetch next ${pageSize} rows only") List<Goods> getListByPage(int pageSize,int pageNo); @Select("select * from TA_TestGoods where categoryId=#{categoryId} order by id") List<Goods> getList(int categoryId); @Select("<script>select * from TA_TestGoods where id in " +"<foreach item='item' index='index' collection='ids' open='(' separator=',' close=')'>#{item}</foreach>" +"order by id</script>") List<Goods> getListByMultIds(@Param("ids")int...ids); @Select("select * from TA_TestGoods where id=#{id}") Goods get(int id); @Insert(value="insert into TA_TestGoods(title, picture, price, introduction, categoryId, " + "lastEditBy, lastEditTime) values(#{title},#{picture},#{price},#{introduction},#{categoryId},#{lastEditBy},getdate())") @Options(useGeneratedKeys=true,keyProperty="id",keyColumn="id") void insert(Goods goods); @Delete(value="delete from TA_TestGoods where id=#{id}") void delete(int id); @Update("update TA_TestGoods set title=#{title},picture=#{picture},price=#{price},introduction=#{introduction}," + "categoryId=#{categoryId},lastEditBy=#{lastEditBy},lastEditTime=getdate() " + "where id=#{id}") void update(Goods goods); }
如上代碼重點說明:
a.增刪改查,對應的註解是:insert、delete、update、select;
b.SQL註解中的參數佔位符有兩種,一種是:#{xxx},最後會生成?的參數化執行,另外一種是:${xxx} 則最後會直接替換成參數的值,即拼SQL(除非信任參數或一些時間、數字類型,不然不建議這種,存在SQL注入風險);
c.insert時若是有自增ID,則能夠經過添加Options註解,並指定useGeneratedKeys=true,keyProperty="數據實體類的屬性字段名",keyColumn="表自增ID的字段名",這樣當insert成功後會自動回填到數據實體類的自增ID對應的屬性上;
d.若是想要生成in子句查詢,則如上代碼getListByMultIds方法上的select註解中使用<script>xxx<foreach>xx</foreach>xx</script>格式實現,若是想用實現複雜的一對一,一對多,多對多等複雜的查詢,則須要添加results註解並指定相應的關聯關係,同時select SQL語句也應關聯查詢,可參見:https://blog.csdn.net/desert568/article/details/79079151
以上2步即完成一個mapper操做類;
2.2全手寫AVA代碼+Mapper XML實現Mybatis的CRUD;
2.2.1.仍然是在cn.zuowenjun.boot.domain包中定義一個數據實體模型類(ShoppingCart:購物車信息),代碼以下:【注意這裏有一個關聯商品信息的屬性:inGoods】
package cn.zuowenjun.boot.domain; import java.util.Date; public class ShoppingCart { private int id; private String shopper; private int goodsId; private int qty; private Date addedTime; private Goods inGoods; public ShoppingCart() { } public ShoppingCart(int id,String shopper,int goodsId,int qty,Date addedTime) { this.id=id; this.shopper=shopper; this.goodsId=goodsId; this.qty=qty; this.addedTime=addedTime; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getShopper() { return shopper; } public void setShopper(String shopper) { this.shopper = shopper; } public int getGoodsId() { return goodsId; } public void setGoodsId(int goodsId) { this.goodsId = goodsId; } public int getQty() { return qty; } public void setQty(int qty) { this.qty = qty; } public Date getAddedTime() { return addedTime; } public void setAddedTime(Date addedTime) { this.addedTime = addedTime; } public Goods getInGoods() { return inGoods; } public void setInGoods(Goods inGoods) { this.inGoods = inGoods; } }
2.2.2.仍然是在cn.zuowenjun.boot.mapper包中定義數據操做接口(interface),注意這裏只是定義接口,並不包含SQL註解部份,由於這部份將在Mapper的XML代碼中進行配置實現,代碼以下:
package cn.zuowenjun.boot.mapper; import java.util.List; import org.apache.ibatis.annotations.Param; import cn.zuowenjun.boot.domain.*; public interface ShoppingCartMapper { List<ShoppingCart> getList(String shopper); void insert(ShoppingCart shoppingCart); void update(ShoppingCart shoppingCart); void deleteItem(int id); void delete(String shopper); int getBuyCount(String shopper); ShoppingCart get(@Param("shopper") String shopper,@Param("goodsId") int goodsId); }
如上代碼有一個重點說明:get方法有兩個參數(多個參數也相似),爲了mybatis可以自動映射到這些參數,必需爲每一個參數添加Param註解,並指定參數名,這個參數名是與對應的Mapper XML中的SQL語句中定義的參數名相同。
2.2.3.在mybatis.mapper-locations設置的mapper xml存放的路徑中建立XML文件,並手動編寫映射的SQL語句,以下所示:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="cn.zuowenjun.boot.mapper.ShoppingCartMapper"> <resultMap id="shoppingCartMap" type="ShoppingCart" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="shopper" property="shopper" jdbcType="NVARCHAR" /> <result column="goodsId" property="goodsId" jdbcType="INTEGER" /> <result column="qty" property="qty" jdbcType="INTEGER"/> <result column="addedTime" property="addedTime" jdbcType="DATE" /> <!-- referseee https://www.cnblogs.com/ysocean/p/7237499.html --> <association property="inGoods" javaType="cn.zuowenjun.boot.domain.Goods"> <id column="id" property="id" jdbcType="INTEGER" /> <result column="title" property="title" /> <result column="picture" property="picture" /> <result column="price" property="price" /> <result column="introduction" property="introduction" /> <result column="categoryId" property="categoryId" /> <result column="lastEditBy" property="lastEditBy" /> <result column="lastEditTime" property="lastEditTime" /> </association> </resultMap> <!-- 若是返回的結果與某個實體類徹底相同,其實徹底不須要上面的resultMap,而是直接使用resultType=類名, 如:resultType=cn.zuowenjun.boot.domain.ShoppingCart(簡寫別名:ShoppingCart),此處是示例用法,故採起指定映射 --> <select id="getList" parameterType="string" resultMap="shoppingCartMap"> select * from TA_TestShoppingCart a inner join TA_TestGoods b on a.goodsId=b.id where shopper=#{shopper} order by addedTime </select> <select id="getBuyCount" parameterType="string" resultType="int"> select count(1) from (select goodsId from TA_TestShoppingCart where shopper=#{shopper} group by goodsId) as t </select> <select id="get" resultMap="shoppingCartMap"> select * from TA_TestShoppingCart a inner join TA_TestGoods b on a.goodsId=b.id where shopper=#{shopper} and goodsId=#{goodsId} </select> <insert id="insert" parameterType="ShoppingCart" useGeneratedKeys="true" keyProperty="id" keyColumn="id"> insert into TA_TestShoppingCart(shopper, goodsId, qty, addedTime) values(#{shopper},#{goodsId},#{qty},getdate()) </insert> <update id="update" parameterType="ShoppingCart" > update TA_TestShoppingCart set shopper=#{shopper},goodsId=#{goodsId},qty=#{qty},addedTime=getdate() where id=#{id} </update> <delete id="deleteItem" parameterType="int"> delete from TA_TestShoppingCart where id=#{id} </delete> <delete id="delete" parameterType="string"> delete from TA_TestShoppingCart where shopper=#{shopper} </delete> </mapper>
如上XML重點說明:
a.凡是使用到類型的地方,能夠在mybatis-config.xml中提早配置類型別名,以簡化配置,固然mybatis已默認設置了一些別名以減小你們配置的工做量,如:string,對應的類型是String等,詳見:http://www.mybatis.org/mybatis-3/zh/configuration.html#typeAliases
b.因爲這個ShoppingCart有關聯屬性:inGoods,故在查詢時都會關聯查詢goods表並經過在resultMap中經過association 元素來指定關聯關係,更多複雜的XML配置詳見:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html
以上3步即完成一個mapper操做類,相比直接使用mapper接口+SQL註解多了一個步驟,但這樣的好處是因爲沒有寫死在代碼中,能夠很容易的更改mapper的相關SQL語句,減小代碼改動量。
2.3使用Mybatis Generator的Maven插件自動生成Mybatis的CRUD;
經過上面的介紹,咱們知道有2種方法來實現一個mapper數據操做類(dao),顯然第2種更能適應更改的狀況,但因爲手寫mapper xml文件很是的麻煩,故能夠經過Mybatis Generator組件,自動生成相關的代碼及xml(通常是:數據實體類domain、數據處理接口mapper、mapper XML),具體實現步驟以下:(能夠單獨一個項目來生成這些文件,也能夠集成在一個項目中,因爲是演示,我這裏是集成在一個項目中)
2.3.1.因爲要使用Mybatis Generator組件,故須要添加對應的JAR包依賴,以下所示:
<!--SQL SERVER 數據驅動,以提供數據訪問支持--> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>7.0.0.jre8</version><!--$NO-MVN-MAN-VER$ --> </dependency> <!-- 添加mybatis生成器,以便經過maven build自動生成model、mapper及XML --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> </dependency>
同時須要添加對應的maven插件,以便經過maven命令可執行生成過程,以下:(經過configurationFile元素指定生成器的配置路徑,overwrite元素指定是否覆蓋生成,這裏有個坑,後面會介紹到,此處略)
<build> <plugins> <plugin> <!--ref: https://gitee.com/free/Mybatis_Utils/blob/master/MybatisGeneator/MybatisGeneator.md --> <!--ref: https://www.cnblogs.com/handsomeye/p/6268513.html --> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <configurationFile>src/main/resources/mybatis/generatorconfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> </plugin> </plugins> </build>
2.3.2.在cn.zuowenjun.boot.domain包中定義相關的數據實體模型類,我這裏演示的類是:ShoppingOrder(購物訂單信息),代碼以下:
package cn.zuowenjun.boot.domain; import java.math.BigDecimal; import java.util.Date; public class ShoppingOrder { private Integer id; private String shopper; private Integer totalqty; private BigDecimal totalprice; private Boolean iscompleted; private String createby; private Date createtime; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getShopper() { return shopper; } public void setShopper(String shopper) { this.shopper = shopper == null ? null : shopper.trim(); } public Integer getTotalqty() { return totalqty; } public void setTotalqty(Integer totalqty) { this.totalqty = totalqty; } public BigDecimal getTotalprice() { return totalprice; } public void setTotalprice(BigDecimal totalprice) { this.totalprice = totalprice; } public Boolean getIscompleted() { return iscompleted; } public void setIscompleted(Boolean iscompleted) { this.iscompleted = iscompleted; } public String getCreateby() { return createby; } public void setCreateby(String createby) { this.createby = createby == null ? null : createby.trim(); } public Date getCreatetime() { return createtime; } public void setCreatetime(Date createtime) { this.createtime = createtime; } }
2.3.3.配置generatorconfig.xml,指定生成的各個細節,因爲generatorconfig的配置節點比較多,以下只是貼出當前示例的配置信息,有一點要說明,注意配置節點的順序,若是順序不對就會報錯,完整的配置方法詳情介紹可參見:https://gitee.com/free/Mybatis_Utils/blob/master/MybatisGeneator/MybatisGeneator.md 或 http://www.javashuo.com/article/p-mvxaarni-co.html
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="application.properties" /> <!-- https://blog.csdn.net/zsy3313422/article/details/53190613 --> <classPathEntry location="E:/LocalMvnRepositories/com/microsoft/sqlserver/mssql-jdbc/7.0.0.jre8/mssql-jdbc-7.0.0.jre8.jar" /> <context id="my" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="javaFileEncoding" value="UTF-8" /> <commentGenerator> <property name="suppressAllComments" value="true" /> <property name="suppressDate" value="true" /> </commentGenerator> <jdbcConnection driverClass="${spring.datasource.driverClassName}" connectionURL="${spring.datasource.url}" userId="${spring.datasource.username}" password="${spring.datasource.password}"> </jdbcConnection> <!-- 生成model實體類文件位置 --> <javaModelGenerator targetPackage="cn.zuowenjun.boot.domain" targetProject="src/main/java"> <property name="enableSubPackages" value="false" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- 生成mapper.xml配置文件位置 --> <!-- targetPackage這裏指定包名,則會在以下的路徑中生成多層級目錄 --> <sqlMapGenerator targetPackage="mybatis.mapper" targetProject="src/main/resources"> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <!-- 生成mapper接口文件位置 --> <javaClientGenerator targetPackage="cn.zuowenjun.boot.mapper" targetProject="src/main/java" type="XMLMAPPER"> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <table tableName="TA_TestShoppingOrder" domainObjectName="ShoppingOrder"> <generatedKey column="id" sqlStatement="JDBC" identity="true" /><!-- 指示ID爲自增ID列,並在插入後返回該ID --> </table> </context> </generatorConfiguration>
因爲涉及的知識點比較多,在此就不做介紹,請參見我給出的連接加以瞭解。
2.3.4.經過maven 插件來執行生成代碼(生成代碼有不少種方法,詳見:http://www.javashuo.com/article/p-gkvefehv-bk.html),這裏我使用最爲方便的一種,步驟以下:
項目右鍵-》RunAs或者DeBug-》Maven Build...-》在goals(階段)中輸入:mybatis-generator:generate,即:設置生成階段,最後點擊Apply或直接Run便可,如圖示:
執行生成後,會在控制檯中顯示最終的結果,以下圖示:若是成功會顯示buid success,並會在相應的目錄中生成對應的文件
2.4進階用法:自定義Mybatis Generator的生成過程當中的插件類,以便添加額外自定義的方法
雖然使用Mybatis Generator減小了手工編寫代碼及XML的工做量,但因爲生成的CRUD方法都是比較簡單的,稍微複雜或靈活一點的方法都不能簡單生成,若是單純的在生成代碼後再人工手動添加其它自定義的方法,又擔憂若是執行一次自動生成又會覆蓋手動添加的自定義代碼,那有沒有辦法解決呢?固然是有的,我(夢在旅途,zuowenjun.cn)在網絡上了解到的方法大部份都是說獲取Mybatis Generator源代碼,而後進行二次開發,最後使用「定製版」的Mybatis Generator,我我的以爲雖然能解決問題,但若是能力不足,可能會出現意想不到的問題,並且進行定製也不是那麼簡單的,故我這裏採起Mybatis Generator框架提供的可擴展插件plugin來實現擴展,具體步驟以下:
2.4.1.在項目新建立一個包cn.zuowenjun.boot.mybatis.plugin,而後在包裏面先建立一個泛型通用插件基類(CustomAppendMethodPlugin),這個基類主要是用於附加自定義方法,故取名CustomAppendMethodPlugin,代碼以下:
package cn.zuowenjun.boot.mybatis.plugin; import java.util.List; import org.mybatis.generator.api.IntrospectedTable; import org.mybatis.generator.api.PluginAdapter; import org.mybatis.generator.api.dom.java.Interface; import org.mybatis.generator.api.dom.java.TopLevelClass; import org.mybatis.generator.api.dom.xml.Document; import org.mybatis.generator.codegen.mybatis3.javamapper.elements.AbstractJavaMapperMethodGenerator; import org.mybatis.generator.codegen.mybatis3.xmlmapper.elements.AbstractXmlElementGenerator; /* * 自定義通用可添加生成自定義方法插件類 * Author:zuowenjun * Date:2019-1-29 */ public abstract class CustomAppendMethodPlugin<TE extends AbstractXmlElementGenerator,TM extends AbstractJavaMapperMethodGenerator> extends PluginAdapter { protected final Class<TE> teClass; protected final Class<TM> tmClass; @SuppressWarnings("unchecked") public CustomAppendMethodPlugin(Class<? extends AbstractXmlElementGenerator> teClass, Class<? extends AbstractJavaMapperMethodGenerator> tmClass) { this.teClass=(Class<TE>) teClass; this.tmClass=(Class<TM>) tmClass; } @Override public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) { try { AbstractXmlElementGenerator elementGenerator = teClass.newInstance(); elementGenerator.setContext(context); elementGenerator.setIntrospectedTable(introspectedTable); elementGenerator.addElements(document.getRootElement()); } catch (InstantiationException | IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.sqlMapDocumentGenerated(document, introspectedTable); } @Override public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable) { try { AbstractJavaMapperMethodGenerator methodGenerator = tmClass.newInstance(); methodGenerator.setContext(context); methodGenerator.setIntrospectedTable(introspectedTable); methodGenerator.addInterfaceElements(interfaze); } catch (InstantiationException | IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.clientGenerated(interfaze, topLevelClass, introspectedTable); } @Override public boolean validate(List<String> warnings) { // TODO Auto-generated method stub return true; } }
代碼比較簡單,主要是重寫了sqlMapDocumentGenerated(生成mapper xml方法)、clientGenerated(生成mapper 接口方法),在這裏面我經過把指定泛型類型(分別繼承自 AbstractXmlElementGenerator、AbstractJavaMapperMethodGenerator)加入到生成XML和接口的過程當中,以實現生成過程的抽象。
2.4.2.我這裏因爲默認生成的ShoppingOrderDetailMapper(實體類:ShoppingOrderDetail是購物訂單詳情)沒法知足須要,我須要額外再增長兩個方法:
List<ShoppingOrderDetail> selectByOrderId(int shoppingOrderId); 、void deleteByOrderId(int shoppingOrderId); 故在這裏自定義繼承自CustomAppendMethodPlugin的插件類:ShoppingOrderDetailMapperPlugin,具體實現代碼以下:
package cn.zuowenjun.boot.mybatis.plugin; import java.util.Set; import java.util.TreeSet; import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType; import org.mybatis.generator.api.dom.java.Interface; import org.mybatis.generator.api.dom.java.JavaVisibility; import org.mybatis.generator.api.dom.java.Method; import org.mybatis.generator.api.dom.java.Parameter; import org.mybatis.generator.api.dom.xml.Attribute; import org.mybatis.generator.api.dom.xml.TextElement; import org.mybatis.generator.api.dom.xml.XmlElement; import org.mybatis.generator.codegen.mybatis3.javamapper.elements.AbstractJavaMapperMethodGenerator; import org.mybatis.generator.codegen.mybatis3.xmlmapper.elements.AbstractXmlElementGenerator; /* * ref see https://www.cnblogs.com/se7end/p/9293755.html * Author:zuowenjun * Date:2019-1-29 */ public class ShoppingOrderDetailMapperPlugin extends CustomAppendMethodPlugin<ShoppingOrderDetailXmlElementGenerator, AbstractJavaMapperMethodGenerator> { public ShoppingOrderDetailMapperPlugin() { super(ShoppingOrderDetailXmlElementGenerator.class,ShoppingOrderDetailJavaMapperMethodGenerator.class); } } class ShoppingOrderDetailXmlElementGenerator extends AbstractXmlElementGenerator{ @Override public void addElements(XmlElement parentElement) { if(!introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime().equalsIgnoreCase("TA_TestShoppingOrderDetail")) { return; } TextElement selectText = new TextElement("select * from " + introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime() + " where shoppingOrderId=#{shoppingOrderId}"); XmlElement selectByOrderId = new XmlElement("select"); selectByOrderId.addAttribute(new Attribute("id", "selectByOrderId")); selectByOrderId.addAttribute(new Attribute("resultMap", "BaseResultMap")); selectByOrderId.addAttribute(new Attribute("parameterType", "int")); selectByOrderId.addElement(selectText); parentElement.addElement(selectByOrderId); TextElement deleteText = new TextElement("delete from " + introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime() + " where shoppingOrderId=#{shoppingOrderId}"); XmlElement deleteByOrderId = new XmlElement("delete"); deleteByOrderId.addAttribute(new Attribute("id", "deleteByOrderId")); deleteByOrderId.addAttribute(new Attribute("parameterType", "int")); deleteByOrderId.addElement(deleteText); parentElement.addElement(deleteByOrderId); } } class ShoppingOrderDetailJavaMapperMethodGenerator extends AbstractJavaMapperMethodGenerator{ @Override public void addInterfaceElements(Interface interfaze) { if(!introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime().equalsIgnoreCase("TA_TestShoppingOrderDetail")) { return; } addInterfaceSelectByOrderId(interfaze); addInterfaceDeleteByOrderId(interfaze); } private void addInterfaceSelectByOrderId(Interface interfaze) { // 先建立import對象 Set<FullyQualifiedJavaType> importedTypes = new TreeSet<FullyQualifiedJavaType>(); // 添加Lsit的包 importedTypes.add(FullyQualifiedJavaType.getNewListInstance()); // 建立方法對象 Method method = new Method(); // 設置該方法爲public method.setVisibility(JavaVisibility.PUBLIC); // 設置返回類型是List FullyQualifiedJavaType returnType = FullyQualifiedJavaType.getNewListInstance(); FullyQualifiedJavaType listArgType = new FullyQualifiedJavaType(introspectedTable.getBaseRecordType()); returnType.addTypeArgument(listArgType); // 方法對象設置返回類型對象 method.setReturnType(returnType); // 設置方法名稱爲咱們在IntrospectedTable類中初始化的 「selectByOrderId」 method.setName("selectByOrderId"); // 設置參數類型是int類型 FullyQualifiedJavaType parameterType; parameterType = FullyQualifiedJavaType.getIntInstance(); // import參數類型對象(基本類型其實能夠沒必要引入包名) //importedTypes.add(parameterType); // 爲方法添加參數,變量名稱record method.addParameter(new Parameter(parameterType, "shoppingOrderId")); //$NON-NLS-1$ // context.getCommentGenerator().addGeneralMethodComment(method, introspectedTable); if (context.getPlugins().clientSelectByPrimaryKeyMethodGenerated(method, interfaze, introspectedTable)) { interfaze.addImportedTypes(importedTypes); interfaze.addMethod(method); } } private void addInterfaceDeleteByOrderId(Interface interfaze) { // 建立方法對象 Method method = new Method(); // 設置該方法爲public method.setVisibility(JavaVisibility.PUBLIC); // 設置方法名稱爲咱們在IntrospectedTable類中初始化的 「deleteByOrderId」 method.setName("deleteByOrderId"); // 設置參數類型是int類型 FullyQualifiedJavaType parameterType; parameterType = FullyQualifiedJavaType.getIntInstance(); method.addParameter(new Parameter(parameterType, "shoppingOrderId")); //$NON-NLS-1$ context.getCommentGenerator().addGeneralMethodComment(method, introspectedTable); if (context.getPlugins().clientSelectByPrimaryKeyMethodGenerated(method, interfaze, introspectedTable)) { interfaze.addMethod(method); } } }
從如上代碼所示,核心點是自定義繼承自AbstractXmlElementGenerator、AbstractJavaMapperMethodGenerator的ShoppingOrderDetailXmlElementGenerator(XML生成器類)、ShoppingOrderDetailJavaMapperMethodGenerator(mapper接口生成器類),而後分別在addElements、addInterfaceElements添加自定義生成XML及接口方法的邏輯(如上代碼中使用的是反射,若想學習瞭解反射請自行網上查找相關資料,C#也有反射哦,應該好理解),注意因爲插件在生成過程當中每一個實體類都會調用一次,故必需做相應的判斷(判斷當前要附加的自定義方法是符與當前實體類生成過程相符,若是不相符則忽略退出)
以下是ShoppingOrderDetail實體類的代碼:
package cn.zuowenjun.boot.domain; import java.math.BigDecimal; import java.util.Date; public class ShoppingOrderDetail { private Integer id; private Integer shoppingorderid; private Integer goodsid; private Integer qty; private BigDecimal totalprice; private String createby; private Date createtime; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getShoppingorderid() { return shoppingorderid; } public void setShoppingorderid(Integer shoppingorderid) { this.shoppingorderid = shoppingorderid; } public Integer getGoodsid() { return goodsid; } public void setGoodsid(Integer goodsid) { this.goodsid = goodsid; } public Integer getQty() { return qty; } public void setQty(Integer qty) { this.qty = qty; } public BigDecimal getTotalprice() { return totalprice; } public void setTotalprice(BigDecimal totalprice) { this.totalprice = totalprice; } public String getCreateby() { return createby; } public void setCreateby(String createby) { this.createby = createby == null ? null : createby.trim(); } public Date getCreatetime() { return createtime; } public void setCreatetime(Date createtime) { this.createtime = createtime; } }
另外順便解決一個踩坑點:上面提到了,咱們在POM文件配置mybatis-generator-maven-plugin插件時,overwrite設爲true,目的是確保每次執行生成時,生成的代碼可以覆蓋已經存在的,理想是美好的,但現實總會有點小意外,咱們這樣配置,只能解決生成的mapper 接口類文件不會重複,但生成的mapper xml文件仍然會附加代碼致使重複,故咱們須要解決這個問題,而解決這個問題的關鍵是:GeneratedXmlFile.isMergeable,若是isMergeable爲true則會合並,目前默認都是false,因此咱們只需實現將GeneratedXmlFile.isMergeable設爲true便可,因爲isMergeable是私有字段,只能採起插件+反射動態改變這個值了,自定義合併代碼插件OverIsMergeablePlugin實現以下:
package cn.zuowenjun.boot.mybatis.plugin; import java.lang.reflect.Field; import java.util.List; import org.mybatis.generator.api.GeneratedXmlFile; import org.mybatis.generator.api.IntrospectedTable; import org.mybatis.generator.api.PluginAdapter; /* * 修復mybatis-generator重複執行時生成的XML有重複代碼(核心:isMergeable=false) * Author:https://blog.csdn.net/zengqiang1/article/details/79381418 * Editor:zuowenjun */ public class OverIsMergeablePlugin extends PluginAdapter { @Override public boolean validate(List<String> warnings) { return true; } @Override public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) { try { Field field = sqlMap.getClass().getDeclaredField("isMergeable"); field.setAccessible(true); field.setBoolean(sqlMap, false); } catch (Exception e) { e.printStackTrace(); } return true; } }
2.4.3.在generatorconfig.xml配置文件中增長plugin配置,以下:
<plugin type="cn.zuowenjun.boot.mybatis.plugin.OverIsMergeablePlugin"></plugin> <plugin type="cn.zuowenjun.boot.mybatis.plugin.ShoppingOrderDetailMapperPlugin"></plugin> ... ...省略中間過程 <table tableName="TA_TestShoppingOrderDetail" domainObjectName="ShoppingOrderDetail"> <generatedKey column="id" sqlStatement="JDBC" identity="true" /> </table>
2.4.4.因爲不能在同一個項目中直接使用plugin類(具體緣由請上網查詢,在此瞭解便可),故還需把cn.zuowenjun.boot.mybatis.plugin這個包中的文件單獨導出生成JAR包,而後把這個JAR包複製到項目的指定目錄下(本示例是放在libs目錄下),而後再在POM爲mybatis-generator-maven-plugin單獨添加system本地依賴才行,maven添加依賴以下:
<plugin> <!--ref: https://gitee.com/free/Mybatis_Utils/blob/master/MybatisGeneator/MybatisGeneator.md --> <!--ref: https://www.cnblogs.com/handsomeye/p/6268513.html --> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <configurationFile>src/main/resources/mybatis/generatorconfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <dependencies> <!-- 爲mybatis-generator增長自定義插件依賴 --> <dependency> <groupId>cn.zuowenjun.boot.mybatis.plugin</groupId> <artifactId>cn.zuowenjun.boot.mybatis.plugin</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${basedir}/src/main/libs/cn.zuowenjun.boot.mybatis.plugin.jar</systemPath> </dependency> </dependencies> </plugin>
若是4步完成後,最後執行maven buid的生成mybatis代碼過程便可,最後查看生成的mapper及xml都會有對應的自定義方法,在此就再也不貼出結果了。
2.5進階用法:利用Mybatis的繼承機制實現添加額外自定義方法
如2.4節所述,咱們能夠經過自定義plugin來實現添加額外自定義的方法,並且不用擔憂被覆蓋,但可能實現有點麻煩(裏面用到了反射),有沒有簡單一點的辦法呢?固然有,便可以先使用Mybatis Generator框架生成默代代碼,而後再結合使用2.2所述方法(手寫mapper接口類及mapper XML),利用mapper XML的繼承特性完成添加自定義方法的過程當中,具體步驟與2.2相同,在此貼出(注意前提是先自動生成代碼,而後再操做以下步驟)
2.5.1.定義擴展mapper接口類(ShoppingOrderExtMapper,擴展ShoppingOrderMapper,它們之間無需繼承),代碼以下:(很簡單,就是定義了一個特殊用途的方法)
package cn.zuowenjun.boot.mapper; import java.util.List; import cn.zuowenjun.boot.domain.ShoppingOrder; public interface ShoppingOrderExtMapper { List<ShoppingOrder> selectAllByShopper(String shopper); }
2.5.2.編寫對應的ShoppingOrderExtMapper.xml,這裏面就要用到繼承,繼承主要是resultMap【實現繼承用:extends=要繼承的mapper xml resultMap】,這樣就不用兩個地方都爲一個實體類寫結果映射配置了,其他的都按一個新的mapper XML配置來設計便可,代碼以下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="cn.zuowenjun.boot.mapper.ShoppingOrderExtMapper"> <resultMap id="BaseResultMap" type="cn.zuowenjun.boot.domain.ShoppingOrder" extends="cn.zuowenjun.boot.mapper.ShoppingOrderMapper.BaseResultMap"> </resultMap> <select id="selectAllByShopper" resultMap="BaseResultMap" parameterType="string"> select * from TA_TestShoppingOrder where shopper=#{shopper} </select> </mapper>
如上兩步即完成擴展添加額外自定義的方法,又不用擔憂重複執行生成代碼會被覆蓋掉,只是使用時須要單獨註冊到spring,單獨實例,雖不完美但彌補了默認生成代碼的不足也是可行的。
2.6 使用SpringBootTest + junit測試基於Mybatis框架實現的DAO類
在此不詳情說明junit測試的用法,網上大把資源,只是單獨說明結合SpringBootTest 註解,完成單元測試,先看單元測試代碼:
package cn.zuowenjun.springbootdemo; import java.math.BigDecimal; import java.util.Date; import java.util.List; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; import cn.zuowenjun.boot.SpringbootdemoApplication; import cn.zuowenjun.boot.domain.*; import cn.zuowenjun.boot.mapper.GoodsMapper; import cn.zuowenjun.boot.mapper.ShoppingOrderDetailMapper; import cn.zuowenjun.boot.mapper.ShoppingOrderMapper; @RunWith(SpringRunner.class) @SpringBootTest(classes=SpringbootdemoApplication.class) public class ShoppingOrderMapperTests { @Autowired private ShoppingOrderMapper shoppingOrderMapper; @Autowired private ShoppingOrderDetailMapper shoppingOrderDetailMapper; @Autowired private GoodsMapper goodsMapper; @Transactional @Rollback(false) //不加這個,默認測試完後自動回滾 @Test public void testInsertShoppingOrder() { Goods goods= goodsMapper.get(1); ShoppingOrder shoppingOrder=new ShoppingOrder(); shoppingOrder.setShopper("zuowenjun"); shoppingOrder.setIscompleted(false); shoppingOrder.setTotalprice(BigDecimal.valueOf(0)); shoppingOrder.setTotalqty(1); shoppingOrder.setCreateby("zuowenjun"); shoppingOrder.setCreatetime(new Date()); int orderId= shoppingOrderMapper.insert(shoppingOrder); shoppingOrder.setId(orderId); ShoppingOrderDetail shoppingOrderDetail=new ShoppingOrderDetail(); shoppingOrderDetail.setGoodsid(goods.getId()); shoppingOrderDetail.setShoppingorderid(shoppingOrder.getId()); shoppingOrderDetail.setQty(10); shoppingOrderDetail.setTotalprice(BigDecimal.valueOf(shoppingOrderDetail.getQty()).multiply(goods.getPrice())); shoppingOrderDetail.setCreateby("zuowenjun"); shoppingOrderDetail.setCreatetime(new Date()); shoppingOrderDetailMapper.insert(shoppingOrderDetail); List<ShoppingOrderDetail> orderDetails= shoppingOrderDetailMapper.selectByOrderId(shoppingOrder.getId()); if(orderDetails!=null && orderDetails.size()>0) { for(ShoppingOrderDetail od:orderDetails) { System.out.println("id:" + od.getId() + ",goodsid:" + od.getGoodsid()); } } Assert.assertTrue(orderDetails.size()>0); } }
與Junit單元測試用法基本相同,惟 一的區別就是在單元測試的類上添加@SpringBootTest,並指定啓動類(如代碼中所示:@SpringBootTest(classes=SpringbootdemoApplication.class)),另外注意一點:若是測試方法使用@Transactional註解,那麼當測試完成後會回滾(即並不會提交事務),若是想完成事務的提交,則需如代碼中所示添加@Rollback(false),其中false指不回滾,true則爲回滾。
3、簡單演示集成Thymeleaf模板引擎(這裏只是用一個簡單的頁面演示效果,因爲如今都流行先後端分離,故只需瞭解便可)
說明:Thymeleaf是spring MVC的端視圖引擎,與JSP視圖引擎相似,只不過在spring boot項目中默認支持Thymeleaf(Thymeleaf最大的優勢是視圖中不含JAVA代碼,不影響UI美工及前端設計),而JSP不建議使用,固然也能夠經過添加相關的JSP的JAR包依賴,實現JSP視圖,具體請自行網上查找資源,同時spring MVC +JSP視圖的用法能夠參見該系列的上篇文章
3.1.添加Thymeleaf的maven依賴,POM配置以下:
<!-- 添加thymeleaf模板引擎(用於springMVC模式,若是是rest API項目,則無需引用) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
3.2.編寫後端controller,以便響應用戶請求,代碼以下:(這個與普通spring MVC+JSP相同,區別在VIEW)
package cn.zuowenjun.boot.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import cn.zuowenjun.boot.domain.*; import cn.zuowenjun.boot.service.*; @Controller @RequestMapping("/test") public class TestController { @Autowired private ShopUserService shopUserService; @GetMapping("/userlist") public String list(Model model) { List<ShopUser> users= shopUserService.getAll(); model.addAttribute("title", "測試使用thymeleaf模板引擎展現數據"); model.addAttribute("users", users); //能夠在application.properties添加以下配置,以改變thymeleaf的默認設置 //spring.thymeleaf.prefix="classpath:/templates/" 模板查找路徑 //spring.thymeleaf.suffix=".html" 模板後綴名 return "/test";//默認自動查找路徑:src/main/resources/templates/*.html } }
3.3編寫前端視圖html模板頁面,最後演示效果
HTML視圖頁面代碼:(th:XXX爲Thymeleaf的模板特有的標識符,${xxx}這是SP EL表達式,這個以前講過的,很簡單,不展開說明)
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>test User List -power by thymeleaf</title> <style type="text/css"> table{ border:2px solid blue; border-collapse:collapse; width:98%; } table *{ border:1px solid blue; text-align:center; } thead{ background-color:purple; color:yellow; } th,td{ padding:5px; } #copy{ margin-top:100px; text-align: center; } </style> </head> <body> <h1 th:text="${title}"></h1> <table> <thead> <tr> <th>SeqNo</th> <th>userId</th> <th>nickName</th> <th>depositAmount</th> </tr> </thead> <tbody> <tr th:if="${users}!=null" th:each="user,iterStat:${users}"> <td th:text="${iterStat.index}+1">1</td> <td th:text="${user.userid}">www.zuowenjun.cn</td> <td th:text="${user.nickname}">夢在旅途</td> <td th:text="${user.depositamount}">520</td> </tr> <tr th:unless="${users.size()} gt 0"> <td colspan="4">暫無相關記錄!</td> </tr> </tbody> </table> <p id="copy"> Copyright ©<span th:text="${#dates.format(#dates.createToday(),'yyyy')}"></span> www.zuowenjun.cn and zuowj.cnblogs.com demo all rights. </p> </body> </html>
最後瀏覽:http://localhost:8080/test/userlist,效果以下圖示:
4、利用VUE+SpringMVC Rest API編寫實現先後端分離的電商購物Demo(瀏覽商品、添加購物車、下單、完成)
說明:因爲數據訪問層(或稱:數據持久層)已由Mybatis Generator完成了,如今就只要編寫業務領域服務層(接口層、實現層),API接入層便可完成後端開發,而後再開發設計前端頁面便可(前端與後端交互使用AJAX)
4.1.在cn.zuowenjun.boot.service包中定義相關的業務領域服務接口
//ShopUserService.java package cn.zuowenjun.boot.service; import java.util.List; import cn.zuowenjun.boot.domain.ShopUser; public interface ShopUserService { List<ShopUser> getAll(); ShopUser get(String userId); String getCurrentLoginUser(); String login(String uid,String pwd); void logout(); } //GoodsService.java package cn.zuowenjun.boot.service; import java.util.List; import org.springframework.web.multipart.MultipartFile; import cn.zuowenjun.boot.domain.*; public interface GoodsService { List<Goods> getGoodsListByPage(int pageSize,int pageNo); List<Goods> getGoodsListByCategory(int categoryId); List<Goods> getGoodsListByMultIds(int...goodsIds); Goods getGoods(int id); void insertGoods(Goods goods,MultipartFile uploadGoodsPic); void updateGoods(Goods goods,MultipartFile uploadGoodsPic); void deleteGoods(int id); List<GoodsCategory> getAllGoodsCategoryList(); void insertGoodsCategory(GoodsCategory goodsCategory); void updateGoodsCategory(GoodsCategory goodsCategory); void deleteGoodsCategory(int id); } //ShoppingOrderService.java package cn.zuowenjun.boot.service; import java.util.List; import cn.zuowenjun.boot.domain.*; public interface ShoppingOrderService { ShoppingOrder getShoppingOrder(int id); List<ShoppingOrder> getShoppingOrderList(String shopper); List<ShoppingOrderDetail> getShoppingOrderDetail(int orderId); boolean createShoppingOrderByShopper(String shopper); void insertShoppingOrderWithDetail(ShoppingOrder order,List<ShoppingOrderDetail> orderDetails); void deleteShoppingOrderDetail(int orderDetailId); void deleteShoppingOrderWithDetail(int orderId); void updateShoppingOrder(ShoppingOrder order); List<ShoppingCart> getShoppingCartList(String shopper); int getShoppingCartBuyCount(String shopper); void insertShoppingCart(ShoppingCart shoppingCart); void deleteShoppingCart(int shoppingCartId); void clearShoppingCart(String shopper); }
如上代碼示,我僅定義了三個service接口,分別是:ShopUserService(用戶服務)、GoodsService(商品服務【含:商品類別、商品信息】)、ShoppingOrderService(購物訂單服務【含:購物車、購物訂單、購物訂單明細】),我說過服務層不必定是與DB中的表一 一對應的,而是應該體現服務內聚(即:業務領域),若是單純的與DAO層同樣,一個service與一個dao對應,那就失去了分層的意義,並且還增長了複雜度。我的見解。
4.2在cn.zuowenjun.boot.service.impl包中實現4.1中相關的業務領域服務接口(代碼很簡單,主要是實現接口的一些方法,惟一有點特別是文件上傳,事務,記錄日誌,這些經過代碼就能看明白就再也不詳情描述了)
//ShopUserServiceImpl.java package cn.zuowenjun.boot.service.impl; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import cn.zuowenjun.boot.EShopProperties; import cn.zuowenjun.boot.domain.ShopUser; import cn.zuowenjun.boot.mapper.ShopUserMapper; import cn.zuowenjun.boot.service.ShopUserService; @Service public class ShopUserServiceImpl implements ShopUserService { private ShopUserMapper shopUserMapper; private EShopProperties shopProperties; @Autowired public ShopUserServiceImpl(ShopUserMapper shopUserMapper,EShopProperties shopProperties) { this.shopUserMapper=shopUserMapper; this.shopProperties=shopProperties; } @Override public List<ShopUser> getAll() { return shopUserMapper.selectAll(); } @Override public ShopUser get(String userId) { return shopUserMapper.selectByPrimaryKey(userId); } @Override public String getCurrentLoginUser() { if(getRequest().getSession().getAttribute("loginUser")==null) { return null; } return getRequest().getSession().getAttribute("loginUser").toString(); } @Override public String login(String uid, String pwd) { if(shopProperties.getShopUserId().equalsIgnoreCase(uid) && shopProperties.getShopUserPwd().equals(pwd)) { getRequest().getSession().setAttribute("loginUser", uid); return null; }else { return "用戶名或密碼不正確!"; } } @Override public void logout() { getRequest().getSession().removeAttribute("loginUser"); } private HttpServletRequest getRequest() { HttpServletRequest request= ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); return request; } } //GoodsServiceImpl.java package cn.zuowenjun.boot.service.impl; import java.io.File; import java.io.IOException; import java.util.List; import java.util.UUID; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; import cn.zuowenjun.boot.domain.*; import cn.zuowenjun.boot.mapper.GoodsCategoryMapper; import cn.zuowenjun.boot.mapper.GoodsMapper; import cn.zuowenjun.boot.service.GoodsService; @Service public class GoodsServiceImpl implements GoodsService { private static Logger logger=LoggerFactory.getLogger(GoodsServiceImpl.class); @Autowired private GoodsMapper goodsMapper; @Autowired private GoodsCategoryMapper categoryMapper; @Override public List<Goods> getGoodsListByPage(int pageSize,int pageNo){ return goodsMapper.getListByPage(pageSize, pageNo); } @Override public List<Goods> getGoodsListByCategory(int categoryId) { return goodsMapper.getList(categoryId); } @Override public List<Goods> getGoodsListByMultIds(int... goodsIds) { return goodsMapper.getListByMultIds(goodsIds); } @Override public Goods getGoods(int id) { return goodsMapper.get(id); } @Transactional @Override public void insertGoods(Goods goods, MultipartFile uploadGoodsPic) { String picPath= saveGoodsPic(uploadGoodsPic); if(picPath!=null && !picPath.isEmpty()) { goods.setPicture(picPath); } goodsMapper.insert(goods); GoodsCategory gcate= categoryMapper.get(goods.getCategoryId()); gcate.setGoodsCount(gcate.getGoodsCount()+1); categoryMapper.update(gcate); logger.info("inserted new goods - id:" + goods.getId()); } @Override public void updateGoods(Goods goods,MultipartFile uploadGoodsPic) { String picPath= saveGoodsPic(uploadGoodsPic); if(picPath!=null && !picPath.isEmpty()) { goods.setPicture(picPath); } goodsMapper.update(goods); logger.info("update goods - id:" + goods.getId()); } @Transactional @Override public void deleteGoods(int id) { Goods g= goodsMapper.get(id); goodsMapper.delete(g.getId()); GoodsCategory gcate= categoryMapper.get(g.getCategoryId()); gcate.setGoodsCount(gcate.getGoodsCount()-1); categoryMapper.update(gcate); //若是有圖片,則同時刪除圖片 if(g.getPicture()!=null && !g.getPicture().isEmpty()) { String picPath= getRequest().getServletContext().getRealPath("/") + g.getPicture(); File file = new File(picPath); if(file.exists()) { file.delete(); } } logger.info("deleted goods - id:" + g.getId()); } @Override public List<GoodsCategory> getAllGoodsCategoryList(){ return categoryMapper.getAll(); } @Override public void insertGoodsCategory(GoodsCategory goodsCategory) { categoryMapper.insert(goodsCategory); } @Override public void updateGoodsCategory(GoodsCategory goodsCategory) { categoryMapper.update(goodsCategory); } @Override public void deleteGoodsCategory(int id) { categoryMapper.delete(id); } private String saveGoodsPic(MultipartFile uploadGoodsPic) { if(uploadGoodsPic==null || uploadGoodsPic.isEmpty()) { return null; } String fileName = uploadGoodsPic.getOriginalFilename(); String extName = fileName.substring(fileName.lastIndexOf(".")); String newFileName=UUID.randomUUID().toString()+extName; File file = new File(getFileSavePath(newFileName)); if(!file.exists()) { file.getParentFile().mkdirs(); } try { uploadGoodsPic.transferTo(file); //return file.toURI().toURL().toString(); return getUrlPath(file.getAbsolutePath()); } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } private String getFileSavePath(String fileName) { String realPath =getRequest().getServletContext().getRealPath("/uploadimgs/"); return realPath + fileName; } private String getUrlPath(String filePath) { String rootPath= getRequest().getServletContext().getRealPath("/"); return filePath.replace(rootPath, "").replaceAll("\\\\", "/"); } private HttpServletRequest getRequest() { HttpServletRequest request= ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); return request; } } //ShoppingOrderServiceImpl.java package cn.zuowenjun.boot.service.impl; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import cn.zuowenjun.boot.domain.*; import cn.zuowenjun.boot.mapper.*; import cn.zuowenjun.boot.service.ShoppingOrderService; @Service public class ShoppingOrderServiceImpl implements ShoppingOrderService { @Autowired private ShoppingOrderMapper orderMapper; @Autowired private ShoppingOrderDetailMapper orderDetailMapper; @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private ShoppingOrderExtMapper shoppingOrderExtMapper; @Override public void insertShoppingCart(ShoppingCart shoppingCart) { ShoppingCart cart=shoppingCartMapper.get(shoppingCart.getShopper(), shoppingCart.getGoodsId()); if(cart==null) { shoppingCartMapper.insert(shoppingCart); }else { cart.setQty(cart.getQty()+shoppingCart.getQty()); shoppingCartMapper.update(cart); } } @Override public void deleteShoppingCart(int shoppingCartId) { shoppingCartMapper.deleteItem(shoppingCartId); } @Override public ShoppingOrder getShoppingOrder(int id) { return orderMapper.selectByPrimaryKey(id); } @Override public List<ShoppingOrder> getShoppingOrderList(String shopper) { return shoppingOrderExtMapper.selectAllByShopper(shopper); } @Override public List<ShoppingOrderDetail> getShoppingOrderDetail(int orderId) { return orderDetailMapper.selectByOrderId(orderId); } @Transactional @Override public boolean createShoppingOrderByShopper(String shopper) { List<ShoppingCart> carts= shoppingCartMapper.getList(shopper); if(carts==null || carts.size()<=0) { return false; } int totalQty=0; BigDecimal totalPrc=BigDecimal.valueOf(0); List<ShoppingOrderDetail> orderDetails=new ArrayList<>(); for(ShoppingCart c:carts) { totalQty+=c.getQty(); BigDecimal itemPrc=c.getInGoods().getPrice().multiply(BigDecimal.valueOf(c.getQty())); totalPrc=totalPrc.add(itemPrc); ShoppingOrderDetail od=new ShoppingOrderDetail(); od.setGoodsid(c.getGoodsId()); od.setQty(c.getQty()); od.setTotalprice(itemPrc); od.setCreateby(shopper); od.setCreatetime(new Date()); orderDetails.add(od); } ShoppingOrder order=new ShoppingOrder(); order.setShopper(shopper); order.setTotalqty(totalQty); order.setTotalprice(totalPrc); order.setCreateby(shopper); order.setCreatetime(new Date()); order.setIscompleted(false); insertShoppingOrderWithDetail(order,orderDetails); clearShoppingCart(shopper); return true; } @Transactional @Override public void insertShoppingOrderWithDetail(ShoppingOrder order, List<ShoppingOrderDetail> orderDetails) { orderMapper.insert(order); int orderId=order.getId(); for(ShoppingOrderDetail od:orderDetails) { od.setShoppingorderid(orderId); orderDetailMapper.insert(od); } } @Override public void deleteShoppingOrderDetail(int orderDetailId) { orderDetailMapper.deleteByPrimaryKey(orderDetailId); } @Transactional @Override public void deleteShoppingOrderWithDetail(int orderId) { orderMapper.deleteByPrimaryKey(orderId); orderDetailMapper.deleteByOrderId(orderId); } @Override public void updateShoppingOrder(ShoppingOrder order) { orderMapper.updateByPrimaryKey(order); } @Override public List<ShoppingCart> getShoppingCartList(String shopper) { return shoppingCartMapper.getList(shopper); } @Override public int getShoppingCartBuyCount(String shopper) { return shoppingCartMapper.getBuyCount(shopper); } @Override public void clearShoppingCart(String shopper) { shoppingCartMapper.delete(shopper); } }
4.3編寫基於VUE前端框架實現的相關UI界面
4.3.1.VUE是什麼?如何使用VUE前端框架設計頁面?認真閱讀官方中文教程就能夠了:https://cn.vuejs.org/v2/guide/index.html ,這裏只是着重說明一下,VUE是實現了MVVM框架,使用VUE的核心組件:模板、路由、數據雙向綁定等特性可以設計出很牛逼的SPA(單WEB頁面的多UI交互的應用),本人(夢在旅途)VUE只是初學者,故在本示例中我只是使用VUE的最基本的一些功能屬性(如:el:指定VUE的渲染範圍(綁定的做用域)、data(數據模型MODEL)、computed(動態計算屬性)、created(VUE初始化後觸發的事件)、methods(綁定自定義方法))
4.3.2.因爲採用先後端分離,徹底能夠一個項目全是靜態的VUE HTML模板,另外一個項目是基於spring boot REST Api項目,但這裏是演示,故採起在同一個項目中,我這裏是在webapp目錄下建立相關的HTML視圖頁面,若是不在同一個項目中,注意基於spring boot REST Api項目中須要設置可以容許跨域訪問,全部HTML視圖代碼以下:
index.html(商品列表,主頁)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>夢在旅途的電商小店Demo-Power by Spring Boot+MyBatis-Boot</title> <meta name="author" content="www.zuowenjun.cn" > <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> <style type="text/css"> #catesbox ul li{float:left;padding:5px;margin-right:20px;border:1px solid green;display: inline-block;cursor:pointer;} .clfx {clear:both;display:block;} .gpic{width:100px;height:100px;text-align:center;vertical-align:middle;} #goodsbox table {width:100%;border-collapse:collapse;} #goodsbox table tr >*{border:1px solid blue;padding:5px;} li.active{background-color:orange;font-weight:bold;} #copy{ margin-top:20px; text-align: center; } body{padding-top:51px;} #topbar{height:50px;line-height:50px;margin:0;width:100%;background-color:WhiteSmoke; position: fixed;top:0;border-bottom:1px solid darkgray;text-align: right;} </style> </head> <body> <div id="app"> <div id="topbar"> <a href="/cartlist.html" target="_blank">購物車(已加入商品數量:{{cartCount}})</a> | <a href="/orderlist.html" target="_blank">訂單中心</a> | <a href="/admin.html" target="_blank">管理後臺</a> </div> <h2>商品類目:</h2> <div id="catesbox"> <ul v-for="c in cates"> <li v-on:click="getGoodsListByCategory(c)" v-bind:class="{active:c.categoryName==curcate}">{{c.categoryName}}({{c.goodsCount}})</li> </ul> <div class="clfx"></div> </div> <h2>當前瀏覽的商品分類:<span>{{curcate}}</span></h2> <div id="goodsbox"> <table> <tr> <th>商品圖片</th> <th>商品標題</th> <th>價格</th> <th>操做</th> </tr> <tr v-for="g in goods"> <td><img v-bind:src="g.picture" class="gpic"></td> <td><a v-bind:href="'/detail.html?gid=' + g.id" target="_blank">{{g.title}}</a></td> <td>¥{{g.price}}</td> <td><button v-on:click="addToShoppingCart(g)">加入購物車</button></td> </tr> </table> </div> </div> <p id="copy"> Copyright ©2019 www.zuowenjun.cn and zuowj.cnblogs.com demo all rights. </p> <script type="text/javascript"> var vm = new Vue({ el:"#app", data:{ cartCount:0, cates:[], goods:[], curcate:"ALL" }, created:function(){ var self = this; this.$http.get('/api/categorys').then(function(res){ self.cates=res.body; //alert(JSON.stringify(self.cates)); },function(){ alert("獲取categorys失敗!"); }); this.$http.get('/api/cartlist').then(function(res){ self.cartCount=res.body.length; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取購物車信息失敗!"); }); //按分頁檢索商品列表 this.getGoodsListByPage(10,1); }, methods:{ getGoodsListByCategory:function(cate){ var self = this; //按類別檢索商品列表 this.$http.get('/api/goods/' + cate.id).then(function(res){ self.goods=res.body; self.curcate=cate.categoryName; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取goods失敗!"); }); }, getGoodsListByPage:function(ps,pno){ var self = this; //按分頁檢索商品列表 this.$http.get('/api/goods' +'?pagesize='+ps +'&page=' + pno).then(function(res){ self.goods=res.body; self.curcate="ALL"; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取goods失敗!"); }); }, addToShoppingCart:function(goods){ //加入購物車 var self = this; var qty=prompt('請輸入購買數量',1); this.$http.post('/api/addToShoppingCart',{goodsid:goods.id,goodsqty:qty}).then(function(res){ var rs=res.body; alert(rs.msg); self.cartCount=rs.data.cartCount; },function(){ alert("加入購物車失敗"); }); } } }); </script> </body> </html>
detail.html(商品詳情)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>商品詳情 -夢在旅途的電商小店</title> <meta name="author" content="www.zuowenjun.cn" > <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> <style type="text/css"> .clfx {clear:both;display:block;} .row{width:100%;margin:10px 0;} .lbox{float:left;width:40%;min-height: 100px;} .rbox{float:right;width:50%;} .rbox ul li{margin:50px auto;} body{padding-top:51px;} #topbar{height:50px;line-height:50px;margin:0;width:100%;background-color:WhiteSmoke; position: fixed;top:0;border-bottom:1px solid darkgray;text-align: right;} </style> </head> <body> <div id="app"> <div id="topbar"> <a href="/cartlist.html" target="_blank">購物車(已加入商品數量:{{cartCount}})</a> | <a href="/admin.html" target="_blank">管理後臺</a> </div> <div class="row"> <div class="lbox"> <img :src="goods.picture" style="width:100%;height:100%;margin:0;padding:0;"> </div> <div class="rbox"> <ul> <li><strong>{{goods.title}}</strong></li> <li>價格:¥{{goods.price}}</li> <li>購買數量:<input v-model="buyqty" value="1"></li> <li>購買價格:<span>{{buyprice}}</span></li> <li><button @click="addToShoppingCart">加入購物車</button></li> </ul> </div> <div class="clfx"></div> </div> <div class="row"> <h2>商品詳細描述:</h2> <hr/> <p>{{goods.introduction}}</p> </div> </div> <script type="text/javascript"> var vm=new Vue({ el:"#app", data:{ cartCount:0, buyqty:1, goods:{} }, created:function(){ var gid= getQueryString("gid"); var self = this; this.$http.get('/api/goods-' + gid).then(function(res){ self.goods=res.body; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取goods失敗!"); }); this.$http.get('/api/cartlist').then(function(res){ self.cartCount=res.body.length; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取購物車信息失敗!"); }); }, computed:{ buyprice:function(){ return (this.buyqty * this.goods.price).toFixed(2); } }, methods:{ addToShoppingCart:function(){ //alert(this.buyqty); //加入購物車 var self = this; this.$http.post('/api/addToShoppingCart',{goodsid:this.goods.id,goodsqty:this.buyqty}).then(function(res){ var rs=res.body; alert(rs.msg); self.cartCount=rs.data.cartCount; },function(){ alert("加入購物車失敗"); }); } } }); function getQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; } </script> </body> </html>
cartlist.html(購物車)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>購物車詳情 -夢在旅途的電商小店</title> <meta name="author" content="www.zuowenjun.cn" > <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> <style type="text/css"> .toolbar{margin:10px 5px;} .carttable{width:100%;margin:0px;padding:5px;border:1px solid gray;} .carttable tr >*{border-bottom:1px solid gray;padding:5px;text-align: center;} .buybtn{background-color:green;border:none;width:280px;padding:20px;color:white;font-size:20pt;} #copy{margin-top:20px;text-align: center;} </style> </head> <body> <div id="app"> <div class="toolbar"> <button @click="deleteItem()" :disabled="carts.length==0">移出購物車</button> | <button @click="clearCart()" :disabled="carts.length==0">清空購物車</button> </div> <div> <table class="carttable"> <tr> <th>選擇</th> <th>商品ID</th> <th>商品名稱</th> <th>預購買數量</th> <th>價格</th> <th>添加時間</th> </tr> <tr v-for="c in carts"> <td><input type="checkbox" class="chkitem" @click="checkitem(c,$event.target)" :checked="chkedItemIds.indexOf(c.id)>-1"></td> <td>{{c.goodsId}}</td> <td>{{c.inGoods.title}}</td> <td>{{c.qty}}</td> <td>¥{{(c.inGoods.price * c.qty).toFixed(2)}}</td> <td>{{c.addedTime}}</td> </tr> <tr v-if="carts.length==0" style="text-align: center;"> <td colspan="6">空空如也,趕忙選購商品吧!~</td> </tr> </table> </div> <p style="text-align: center;"> <button class="buybtn" @click="createOrder()" :disabled="carts.length==0">當即下單</button> </p> </div> <p id="copy"> Copyright ©2019 www.zuowenjun.cn and zuowj.cnblogs.com demo all rights. </p> <script type="text/javascript"> var vm=new Vue({ el:"#app", data:{ carts:[], chkedItemIds:[] }, created:function(){ var self = this; this.$http.get('/api/cartlist').then(function(res){ self.carts=res.body; //alert(JSON.stringify(self.carts)); },function(){ alert("獲取購物車信息失敗!"); }); }, methods:{ checkitem:function(cart,chk){ //alert(chk.checked); if(chk.checked){ this.chkedItemIds.push(cart.id); }else{ this.chkedItemIds.remove(cart.id); } }, deleteItem:function(){ var self = this; //alert(JSON.stringify(self.chkedItemIds)); this.$http.post('/api/deletecartitems-many',self.chkedItemIds).then(function(res){ self.carts= self.carts.filter(function(e){ return self.chkedItemIds.indexOf(e.id)<=-1;}); alert(res.body.msg); },function(){ alert("刪除失敗!"); }); }, clearCart:function(){ var self = this; this.$http.post('/api/deletecartitems-all').then(function(res){ self.carts=[]; alert(res.body.msg); },function(){ alert("刪除失敗!"); }); }, createOrder:function(){ var self = this; this.$http.post('/api/createorder').then(function(res){ alert(res.body.msg); if(res.body.code==0){//如查下單成功,則清空購物車 self.carts=[]; } },function(){ alert("下單失敗!"); }); } } }); Array.prototype.remove = function(val) { var index = this.indexOf(val); if (index > -1) { this.splice(index, 1); } }; </script> </body> </html>
orderlist.html(訂單中心)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>訂單詳情 -夢在旅途的電商小店</title> <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> <style type="text/css"> table{border:solid 1px blue;border-collapse: collapse;width:100%;margin:10px 1px;} table tr >*{border:solid 1px blue,padding:5px;border:dotted 1px gray;} .cfmbar{text-align: center;} .cfmbar button{border:none;background-color:blue;color:#ffffff;padding:10px 50px;} #copy{margin-top:20px;text-align: center;} </style> </head> <body> <div id="app"> <div> <h2>訂單列表:</h2> <table> <tr> <th>訂單號</th> <th>商品數量</th> <th>訂單價格</th> <th>完成否(收貨確認)</th> <th>建立時間</th> <th>查看訂單詳情</th> </tr> <tr v-for="o in shoppingOrders"> <td>{{o.id}}</td> <td>{{o.totalqty}}</td> <td>{{o.totalprice.toFixed(2)}}</td> <td>{{o.iscompleted?"已收貨":"待收貨"}}</td> <td>{{o.createtime}}</td> <td><button @click="showOrderDetail(o)">查看</button></td> </tr> <tr v-if="shoppingOrders.length==0" style="text-align: center;"> <td colspan="6">沒有任何訂單信息!</td> </tr> </table> </div> <div v-if="viewOrder!=null"> <h3>訂單號【{{viewOrder.id}}】詳情:</h3> <table> <tr> <th>商品ID</th> <th>商品名稱</th> <th>購買數量</th> <th>費用</th> </tr> <tr v-for="od in viewOrderDetails.details"> <td>{{od.goodsid}}</td> <td>{{goodsName(od)}}</td> <td>{{od.qty}}</td> <td>¥{{od.totalprice.toFixed(2)}}</td> </tr> </table> <p class="cfmbar" v-if="!viewOrder.iscompleted"> <button @click="confirmOrderCompleted(viewOrder)" >確認完成(已收貨)</button> </p> </div> </div> <p id="copy"> Copyright ©2019 www.zuowenjun.cn and zuowj.cnblogs.com demo all rights. </p> <script type="text/javascript"> var vm=new Vue({ el:"#app", data:{ shoppingOrders:[], viewOrder:null, viewOrderDetails:null }, created:function(){ var self = this; this.$http.get('/api/orders').then(function(res){ self.shoppingOrders=res.body; //alert(JSON.stringify(self.shoppingOrders)); },function(){ alert("獲取orders失敗!"); }); }, computed:{ goodsName(){//利用JS閉包實現傳參 return function(od){ var goods= this.viewOrderDetails.goodss.filter(function(g){return g.id==od.goodsid })[0]; //alert(od.goodsid); return goods.title; } } }, methods:{ showOrderDetail:function(o){ var self = this; this.$http.post('/api/orderdetail',{orderId:o.id}).then(function(res){ if(res.body.code==0){ self.viewOrderDetails=res.body.data; //alert(JSON.stringify(self.viewOrderDetails)); }else{ alert(res.body.msg); self.viewOrderDetails=null; o=null; } self.viewOrder=o; },function(){ alert("獲取orderdetail失敗!"); }); }, confirmOrderCompleted:function(o){ var self = this; this.$http.post('/api/confirmOrderCompleted',{orderId:o.id}).then(function(res){ alert(res.body.msg); if(res.body.code==0){ self.viewOrder.iscompleted=true; } }),function(){ alert("確認訂單完成失敗!"); }; } } }); function getQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; } </script> </body> </html>
admin.html(管理後臺,因爲DEMO,故只實現商品的增、刪功能,其他管理功能未實現,僅做演示)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>管理後臺 -夢在旅途的電商小店</title> <meta name="author" content="www.zuowenjun.cn" > <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> <style type="text/css"> table{border:solid 1px blue;border-collapse: collapse;width:100%;margin:10px 1px;} table tr >*{border:solid 1px blue,padding:5px;border:dotted 1px gray;} .gpic{width:100px;height:100px;text-align:center;vertical-align:middle;} </style> </head> <body> <div id="app"> <fieldset> <legend>管理商品:</legend> <table> <colgroup> <col style="width:auto"> <col style="width:auto"> <col style="width:100px"> <col style="width:300px"> <col style="width:auto"> <col style="width:auto"> <col style="width:auto"> <col style="width:auto"> <col style="width:auto"> </colgroup> <tr> <th>商品ID</th> <th>商品名稱</th> <th>商品圖片</th> <th>商品介紹</th> <th>單價</th> <th>類別ID</th> <th>最後編輯者</th> <th>最後編輯時間</th> <th>操做</th> </tr> <tr style="background-color:orange;"> <td>{{editgoods.id}}</td> <td><input type="text" v-model="editgoods.title"></td> <td><img v-bind:src="editgoods.picture" class="gpic"> <input class="upload" type="file" id="gpicfile" @change="selectimg($event.target)" accept="image/png,image/gif,image/jpeg"></td> <td><textarea v-model="editgoods.introduction"></textarea></td> <td><input type="text" v-model="editgoods.price"></td> <td> <select v-model="editgoods.categoryId"> <option v-for="c in categorys" v-bind:value="c.id">{{c.categoryName}}</option> </select> </td> <td>{{editgoods.lastEditBy}}</td> <td>{{editgoods.lastEditTime}}</td> <td><button @click="savegoods(editgoods)">保存</button></td> </tr> <tr v-for="g in goodss"> <td>{{g.id}}</td> <td>{{g.title}}</td> <td><img v-bind:src="g.picture" class="gpic"></td> <td>{{g.introduction}}</td> <td>{{g.price}}</td> <td>{{g.categoryId}}</td> <td>{{g.lastEditBy}}</td> <td>{{g.lastEditTime}}</td> <td><button @click="editgoods(g)" disabled="disabled">修改</button> | <!-- UI暫不實現修改,禁用 --> <button @click="delgoods(g)">刪除</button></td> </tr> </table> </fieldset> </div> <script type="text/javascript"> var vm=new Vue({ el:"#app", data:{ categorys:[], goodss:[], editgoods:{ id:null, title:null, picture:null, price:0.00, introduction:null, categoryId:1, lastEditBy:"zuowenjun", lastEditTime:null } }, created:function(){ this.$http.get('/api/categorys').then(function(res){ this.categorys=res.body; },function(){ alert("獲取categorys失敗!"); }); this.getGoodsListByPage(100,1);//DEMO,只加載第1頁 }, methods:{ getGoodsListByPage:function(ps,pno){ var self = this; //按分頁檢索商品列表 this.$http.get('/api/goods' +'?pagesize='+ps +'&page=' + pno).then(function(res){ self.goodss=res.body; //alert(JSON.stringify(self.goods)); },function(){ alert("獲取goods失敗!"); }); }, selectimg:function(el){ let gpic=el.files[0]; let type=gpic.type;//文件的類型,判斷是不是圖片 let size=gpic.size;//文件的大小,判斷圖片的大小 if('image/gif, image/jpeg, image/png, image/jpg'.indexOf(type) == -1){ alert('請選擇咱們支持的圖片格式!'); return false; } if(size>3145728){ alert('請選擇3M之內的圖片!'); return false; } var uri =''; this.editgoods.picture=URL.createObjectURL(gpic); }, savegoods:function(g){ var fileDom=document.getElementById("gpicfile"); let formData = new FormData(); formData.append('id', this.editgoods.id); formData.append('title', this.editgoods.title); formData.append('picture', fileDom.files[0]); formData.append('price', this.editgoods.price); formData.append('introduction', this.editgoods.introduction); formData.append('categoryId', this.editgoods.categoryId); let config = { headers: { 'Content-Type': 'multipart/form-data' } } this.$http.post('/api/savegoods', formData, config).then(function (res) { alert(res.body.msg); if(res.body.code==0){ this.goodss.unshift(res.body.data);//插入到數組最新面 this.editgoods={//從新初始化,以便實現清空全部編輯框 id:null, title:null, picture:null, price:0.00, introduction:null, categoryId:1, lastEditBy:"zuowenjun", lastEditTime:null }; } }); }, delgoods:function(g){ this.$http.get('/api/delgoods/' + g.id).then(function(res){ alert(res.body.msg); if(res.body.code==0){ this.goodss.remove(g); } },function(){ alert("刪除goods失敗!"); }); } } }); Array.prototype.remove = function(val) { var index = this.indexOf(val); if (index > -1) { this.splice(index, 1); } }; </script> </body> </html>
前端交互所須要API(由於是DEMO,故全部的API ACTION方法都在Apicontroller中),代碼以下:
package cn.zuowenjun.boot.controller; import java.math.BigDecimal; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import cn.zuowenjun.boot.domain.*; import cn.zuowenjun.boot.service.*; /* * ALL REST API */ @RestController @RequestMapping("/api") public class ApiController { @Autowired private GoodsService goodsService; @Autowired private ShoppingOrderService shoppingOrderService; @Autowired private ShopUserService shopUserService; private String getCurrentShopper() { String shopper = shopUserService.getCurrentLoginUser(); return shopper; } @PostMapping(value="/login",produces = "application/json;charset=utf-8") public ApiResultMsg login(@RequestBody Map<String,String> requestMap) { String userid=requestMap.get("userid"); String upwd=requestMap.get("upwd"); String loginResult= shopUserService.login(userid, upwd); if(loginResult==null) { return new ApiResultMsg(0,"OK",null); }else { return new ApiResultMsg(-1,"登陸失敗:" + loginResult,null); } } @GetMapping(value = "/categorys", produces = "application/json;charset=utf-8") public List<GoodsCategory> getAllGoodsCategorys() { return goodsService.getAllGoodsCategoryList(); } @GetMapping(value = "/goods/{cateid}", produces = "application/json;charset=utf-8") public List<Goods> getGoodsList(@PathVariable(name = "cateid") int categoryid) { return goodsService.getGoodsListByCategory(categoryid); } @GetMapping(value = "/goods", produces = "application/json;charset=utf-8") public List<Goods> getGoodsList(@RequestParam(name = "pagesize", required = false) String spageSize, @RequestParam(name = "page", required = false) String spageNo) { int pageSize = tryparseToInt(spageSize); int pageNo = tryparseToInt(spageNo); pageSize = pageSize <= 0 ? 10 : pageSize; pageNo = pageNo <= 1 ? 1 : pageNo; return goodsService.getGoodsListByPage(pageSize, pageNo); } @GetMapping(value = "/goodsmany", produces = "application/json;charset=utf-8") public List<Goods> getGoodsListByMultIds(@RequestBody int[] ids) { return goodsService.getGoodsListByMultIds(ids); } @PostMapping(value = "/addToShoppingCart", produces = "application/json;charset=utf-8") public ApiResultMsg addToShoppingCart(@RequestBody Map<String, Integer> json) { int goodsId = json.get("goodsid"); int qty = json.get("goodsqty"); ApiResultMsg msg = new ApiResultMsg(); if (goodsId <= 0) { msg.setCode(101); msg.setMsg("該商品ID無效"); return msg; } String shopper = getCurrentShopper(); ShoppingCart shoppingCart = new ShoppingCart(0, shopper, goodsId, qty, new Date()); shoppingOrderService.insertShoppingCart(shoppingCart); msg.setCode(0); msg.setMsg("添加購物車成功!"); int cartCount = shoppingOrderService.getShoppingCartBuyCount(shopper); HashMap<String, Object> data = new HashMap<>(); data.put("cartCount", cartCount); msg.setData(data); return msg; } @GetMapping(value = "/goods-{gid}", produces = "application/json;charset=utf-8") public Goods getGoods(@PathVariable("gid") int goodsId) { return goodsService.getGoods(goodsId); } @GetMapping(value = "/cartlist", produces = "application/json;charset=utf-8") public List<ShoppingCart> getShoppingCartList() { String shopper = getCurrentShopper(); return shoppingOrderService.getShoppingCartList(shopper); } @PostMapping(value = "/deletecartitems-{mode}", produces = "application/json;charset=utf-8") public ApiResultMsg deleteShoppingCartItems(@PathVariable("mode") String mode, @RequestBody(required = false) int[] cartIds) { if (mode.equalsIgnoreCase("all")) { String shopper = getCurrentShopper(); shoppingOrderService.clearShoppingCart(shopper); } else { for (int id : cartIds) { shoppingOrderService.deleteShoppingCart(id); } } return new ApiResultMsg(0, "刪除成功!", null); } @PostMapping(value = "/createorder", produces = "application/json;charset=utf-8") public ApiResultMsg createShoppingOrder() { String shopper = getCurrentShopper(); ApiResultMsg msg = new ApiResultMsg(); if (shoppingOrderService.createShoppingOrderByShopper(shopper)) { msg.setCode(0); msg.setMsg("恭喜你,下單成功!"); } else { msg.setCode(101); msg.setMsg("對不起,下單失敗,請重試!"); } return msg; } @RequestMapping(path = "/orders", produces = "application/json;charset=utf-8", method = RequestMethod.GET) // 等同於@GetMapping public List<ShoppingOrder> getShoppingOrderList() { String shopper = getCurrentShopper(); return shoppingOrderService.getShoppingOrderList(shopper); } @RequestMapping(path = "/orderdetail", produces = "application/json;charset=utf-8", method = RequestMethod.POST) // 等同於@PostMapping public ApiResultMsg getShoppingOrderDetail(@RequestBody Map<String, String> requestJosn) { String orderId = requestJosn.get("orderId"); List<ShoppingOrderDetail> orderDetails = shoppingOrderService.getShoppingOrderDetail(tryparseToInt(orderId)); ApiResultMsg msg = new ApiResultMsg(); if (orderDetails.size() > 0) { int[] goodsIds = new int[orderDetails.size()]; for (int i = 0; i < orderDetails.size(); i++) { goodsIds[i] = orderDetails.get(i).getGoodsid(); } List<Goods> goodsList = goodsService.getGoodsListByMultIds(goodsIds); HashMap<String, Object> data = new HashMap<>(); data.put("details", orderDetails); data.put("goodss", goodsList); msg.setCode(0); msg.setData(data); } else { msg.setCode(101); msg.setMsg("獲取訂單詳情信息失敗!"); } return msg; } //這裏示例配置多個URL請求路徑 @PostMapping(path= {"/confirmOrderCompleted","/cfmordercompl"},produces="application/json;charset=utf-8") public ApiResultMsg confirmOrderCompleted(@RequestBody Map<String, String> requestJosn) { String reqOrderId = requestJosn.get("orderId"); ApiResultMsg msg=new ApiResultMsg(); try { int orderId=tryparseToInt(reqOrderId); ShoppingOrder order= shoppingOrderService.getShoppingOrder(orderId); order.setIscompleted(true); shoppingOrderService.updateShoppingOrder(order); msg.setCode(0); msg.setMsg("確認訂單完成成功(已收貨)"); }catch (Exception e) { msg.setCode(101); msg.setMsg("確認訂單完成失敗:" + e.getMessage()); } return msg; } @PostMapping(path="/savegoods",produces="application/json;charset=utf-8",consumes="multipart/form-data") public ApiResultMsg saveGoods(@RequestParam("picture") MultipartFile gpic,HttpServletRequest request) { ApiResultMsg msg=new ApiResultMsg(); try { Goods goods=new Goods(); goods.setId(tryparseToInt(request.getParameter("id"))); goods.setTitle(request.getParameter("title")); goods.setPrice(new BigDecimal(request.getParameter("price"))); goods.setIntroduction(request.getParameter("introduction")); goods.setCategoryId(tryparseToInt(request.getParameter("categoryId"))); goods.setLastEditBy(getCurrentShopper()); goods.setLastEditTime(new Date()); if(goods.getId()<=0) { goodsService.insertGoods(goods, gpic); } else { goodsService.updateGoods(goods, gpic); } msg.setCode(0); msg.setMsg("保存成功!"); msg.setData(goods); }catch (Exception e) { msg.setCode(101); msg.setMsg("保存失敗:" + e.getMessage()); } return msg; } @GetMapping(path="/delgoods/{gid}",produces="application/json;charset=utf-8") public ApiResultMsg deleteGoods(@PathVariable("gid") int goodsId) { goodsService.deleteGoods(goodsId); ApiResultMsg msg=new ApiResultMsg(); msg.setCode(0); msg.setMsg("刪除商品成功!"); return msg; } private int tryparseToInt(String str) { try { return Integer.parseInt(str); } catch (Exception e) { return -1; } } }
REST API controller與普通的MVC controller用法上基本相同,只是REST API ACTION返回的是數據內容自己(@RestController或@Controller+@ResponseBody),而MVC ACTION通常返回view
4.4添加身份認證攔截器、日誌記錄等
由於演示的是電商購物場景,既有下單又有後臺管理,故這裏我增長了登陸視圖及登陸攔截器,以完成對部份頁面及API的權限控制,實現代碼以下:
4.4.1.設計login.html(登陸)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登陸入口 -夢在旅途的電商小店</title> <meta name="author" content="www.zuowenjun.cn" > <script src="https://cdn.jsdelivr.net/npm/vue" type="text/javascript"></script> <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script> </head> <body> <div id="app"> <form method="post" @submit.prevent="loginsubmit"> <p>用戶ID:</p> <p><input type="text" v-model="uid"></p> <p>密碼:</p> <p><input type="password" v-model="pwd"></p> <p> <button type="submit">登陸</button> </p> </form> </div> <script type="text/javascript"> var vm=new Vue({ el:"#app", data:{ uid:null, pwd:null }, methods:{ loginsubmit:function(){ this.$http.post('/api/login',{userid:this.uid,upwd:this.pwd}).then(function(res){ var rs=res.body; if(rs.code==0){ window.location.href="index.html"; }else{ alert(rs.msg); } },function(){ alert("登陸請求失敗!"); }); } } }); </script> </body> </html>
4.4.2.自定義實現HandlerInterceptor的登陸驗證攔截器:LoginInterceptor,代碼以下:(注意我是將該攔截器放在根包中cn.zuowenjun.boot)
package cn.zuowenjun.boot; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import com.fasterxml.jackson.databind.ObjectMapper; import cn.zuowenjun.boot.domain.ApiResultMsg; @Component public class LoginInterceptor implements HandlerInterceptor { private static Logger logger = LoggerFactory.getLogger(LoginInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) {// 未登陸則轉到登陸頁面 boolean isAjaxRequest = false; boolean isAcceptJSON = false; if (request.getHeader("x-requested-with") != null && request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) { isAjaxRequest = true; } if (request.getHeader("Accept") != null && request.getHeader("Accept").contains("application/json")) { isAcceptJSON = true; } if(isAjaxRequest || isAcceptJSON) { //使用jackson序列化JSON ApiResultMsg msg=new ApiResultMsg(-1,"未登陸,禁止訪問",null); ObjectMapper mapper = new ObjectMapper(); String msgJson= mapper.writeValueAsString(msg); responseJson(response,msgJson); }else { response.sendRedirect("/login.html"); } return false; } return true; } private void responseJson(HttpServletResponse response, String json) throws Exception { PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("applcation/json; charset=utf-8"); try { writer = response.getWriter(); writer.print(json); } catch (IOException e) { logger.error("response error", e); } finally { if (writer != null) writer.close(); } } }
代碼比較簡單,主要是判斷session中是否有記錄登陸的用戶名,若是沒有則表示未登陸,而後根據是AJAX請求或須要返回JSON的狀況則返回JSON,不然直接跳轉到login.html頁面。
4.4.3.自定義實現WebMvcConfigurer的配置類:SpringbootdemoAppConfigurer,重寫addInterceptors方法,在該方法中把LoginInterceptor實例加入到攔截器管道中,以便交由spring MVC進行管理,代碼以下:(一樣放在根包中)
package cn.zuowenjun.boot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.*; @Configuration public class SpringbootdemoAppConfigurer implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { InterceptorRegistration registration = registry.addInterceptor(loginInterceptor); registration.addPathPatterns("/**"); registration.excludePathPatterns("/*.html","/uploadimgs/*","/error","/api/login","/api/categorys","/api/goods*","/api/goods/*", "/hello/*","/test/*"); } }
注意我這裏是攔截全部路徑,而後使用excludePathPatterns來排除不須要攔截的路徑,若是須要攔截的路徑比較少,能夠直接指定攔截的具體路徑,這樣就不用排除了。
4.4.4.另外補充一個功能點說明:通常一個應用程度都會有日誌記錄,這裏也不能少,spring boot中默認實現了:slf4j+logback(slf4j是一個日誌門面接口,logback是slf4j接口的實現,這樣搭配比較好,能夠隨時更換日誌實現框架),先在application.properties配置日誌的基本參數,以下所示:(詳細集成配置,可參見:https://www.jianshu.com/p/88b03b03570c)
#logging.config=xxxx #能夠指定單獨的日誌配置XML文件,進行更豐富的設置,這裏未採用
logging.level.root=info
logging.level.cn.zuowenjun.boot.mapper=debug
logging.file=springbootdemo.log
配置好後,而後在相應的類中直接使用便可,用法以下:(具體可見上面的GoodsServiceImpl代碼)
private static Logger logger=LoggerFactory.getLogger(GoodsServiceImpl.class);//經過日誌工廠得到一個日誌記錄實例 logger.info("日誌信息");//有多個日誌級別可用,但注意須要配置中的root級別相對應,好比目前是配置了info,若是使用debug,可能就不會輸出日誌到文件
4.5效果展現:(所有采用HTML+AJAX靜態交互)
經過以上的後端API代碼+基於VUE的前端HTML,就完成了一個簡單的電商物購DEMO,效果截圖以下:
主頁:(加入購物車後,右上角的」購物車(已加入商品數量:0)「 數字變自動同步更新)
商品詳情:
購物車:
訂單管理:
後臺管理(商品管理):
添加商品後,自動加入到列表第一行,若是刪除則移除刪除的商品所在行
本文示例項目源碼,請參見GIT:https://github.com/zuowj/springbootdemo