JAVA WEB快速入门之从编写一个基于SpringBoot+My…
2019-02-17 01:49:59来源:博客园 阅读 ()
JAVA WEB快速入门系列之前的相关文章如下:(文章全部本人【梦在旅途原创】,文中内容可能部份图片、代码参照网上资源)
第一篇:JAVA WEB快速入门之环境搭建
第二篇:JAVA WEB快速入门之从编写一个JSP WEB网站了解JSP WEB网站的基本结构、调试、部署
第三篇:JAVA WEB快速入门之通过一个简单的Spring项目了解Spring的核心(AOP、IOC)
第四篇:JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC
今天是第五篇,也是该系列文章的最后一篇,接上篇《JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC》,通过上篇文章的详细介绍,知道如何使用maven来快速构建spring MVC应用,也能够使用spring MVC+springJDBC实现网站开发,而本文所涉及的知识则是在这基础之上继续提升,核心是讲解如何使用spring boot来更快速的构建spring MVC,并通过mybatis及代码生成相关DAO,同时利用VUE前端框架开发前后端分离的网站,用户体验更好,废话不多说,直接进入本文主题。
(提示:本文内容有点长,涉及的知识点也比较多,若是新手建议耐心看完!)
一、创建Spring Boot+SpringMVC空项目
1.1通过https://start.spring.io/官网快速生成一个Spring Boot+SpringMVC空项目,如下图示:
(当然也可以通过Eclipse或IDEA的Spring Boot插件来创建,可参见:https://www.cnblogs.com/shaoniandream/p/9679942.html,https://blog.csdn.net/qq_32572497/article/details/62037873)
设置后点击页面的生成项目按钮,即可生成并下载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架构风格的。
效果如下:
二、使用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 或 https://www.cnblogs.com/handsomeye/p/6268513.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 插件来执行生成代码(生成代码有很多种方法,详见:https://blog.csdn.net/qq_32786873/article/details/78226925),这里我使用最为方便的一种,步骤如下:
项目右键-》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则为回滚。
三、简单演示集成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,效果如下图示:
四、利用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
原文链接:https://www.cnblogs.com/zuowj/p/10335080.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 国外程序员整理的Java资源大全(全部是干货) 2020-06-12
- 2020年深圳中国平安各部门Java中级面试真题合集(附答案) 2020-06-11
- 2020年java就业前景 2020-06-11
- 04.Java基础语法 2020-06-11
- Java--反射(框架设计的灵魂)案例 2020-06-11
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash