1. 前言 WEB系统开发工程师没有听说过Spring Framework的应该不多,但是很多情况,项目的框架都是别人已经搭建好的,只要按照固定的模式填写业务逻辑就可以了。如果让自己从开始做一个系统就不知道从哪里下手。30分钟入门系列计划把常用的各种开发技术,用动手实践的方式重新入门,以防自己知其然不知其所以然。30分钟学会一门新技术比较困难,但是对于有经验的工程师来讲,掌握了基本原理,自己再动一遍手,在真正项目中就能很快的理解那些框架的意图和实现方式。
本次的主题是如何利用Spring Boot2快速搭建一个简单的WEB程序。本文暂时对详细细节不做过多描述,首先按照示例搭建一个可以动起来的程序,在后续的文章中会对使用的每种技术进行说明。
通过本文您可以大概了解如下知识。
・ 典型Spring框架的构成 ・ Thymeleaf页面模版 ・ Bootstrap简单设计 ・ Mybatis数据存储 ・ 简单表单验证
2. Spring Boot 是什么 框架在各种大型系统开发中举足轻重的作用毋庸置疑,各种语言基本上都有各自的开发框架,随着时代的发展这些框架有的走向衰落,有的不断发展壮大。在Java的世界里,Spring Framework基本上已经成了事实上的业界标准。Spring Framework在2002年刚出道的时候还只是一个只实现了DI(Dependency Injection,依存注入)的小框架,发展到现在已经成了一个“无所不能”的大规模综合框架,光看下面的Spring Framework家族已经够眼花缭乱了吧。
图2-1. Spring Framework家族
那么Spring Boot到底是什么呢?简单说,Spring Boot是一个可以简化Spring开发的框架。Spring Boot不是什么新的框架,而是基于Spring Framework默认配置了很多框架的使用方式,并内置服务器,使开发更简单、快捷、方便!
3. 示例程序 我们将利用Spring Boot一起完成下面一个简单的web程序,实现最基本的CRUD(创建、检索、更新、删除)操作,以了解Spring Web应用的经典构成。
图3-1. 商品列表
图3-2. 商品新建
图3-3. 商品详细
图3-4. 商品变更
4. 开发环境 首先准备开发环境,本文采用的都是目前的最新版本,遇到问题查阅资料时,会出现很多旧版本的信息,一部分框架新旧版本不兼容,需要特别注意。
(1) JDK 2018年9月随着Java 11的发布,Oracle对相关的JDK 支持政策进行了调整,从免费改为收费,此后Java收费的话题一度成为技术界争论的焦点。Java的前世今生,以及各种版本如何收费等等我们会在另外的文章中详细阐述。不管Java是不是收费,有一点可以肯定的是,不管是哪个JDK版本,目前学习开发是可以免费使用的。如果您的电脑中尚未安装JDK,可以从下面的链接下载。Windows环境中注意需要修改环境变量。 Oracle JDK 12: https://www.oracle.com/technetwork/java/javase/downloads/index.html
Open JDK 12: https://jdk./12/
(2) Spring Tool Suite 4 工欲善其事,必先利其器。Spring提供了一套完整的基于Eclipse的IDE,另外还有基于Visual Studio Code的插件,基于Atom的插件。使用这些IDE环境,能够很大程度上提高生产性。本文采用STS4进行开发,可以从下面的链接下载OS相应版本。 STS4.2.2:https:///tools
(3) Bootstrap Bootstrap是Twitter推出的一个用于前端开发的开源工具包,即使没有美工的专业水平也能轻松的做出比较好看的布局和各种网页要素。引入Bootstrap可以采用CDN声明的方式也可以把CSS和JavaScript下载到本地进行引入。本文采用本地方式,可以从下面的链接下载最新版本的Bootstrap以及依存的jQuery库。需要说明的是Bootstrap目前最新版本是4,和版本3比较很多tag发生了变化,网上比较多的是版本3的代码,如果拷贝进去不起作用,可以确认是否因为版本不同造成的。 Bootstrap4.3.1: https:/// jQuery3.4.1 :https:///
5. 动手实践 接下来我们一起动手做一个很简单但是很典型的Spring MVC程序。数据库使用了H2,H2数据库是纯Java实现的内存关系数据库,数据不会保存到硬盘,内置服务器每次重新启动数据都会被清空。有兴趣的同学可以改用MySQL、PostgreSQL等数据库。 本示例系统结构如下图所示。
本示例最终构成如下图所示。
在实际项目开发中,一般都是先进行对象整理,抽取各种属性,进行数据库及画面设计等等。本文为了减少篇幅,忽略各种设计过程,直接按照MVC的顺序讲解一个简单程序的构建过程。
(1) 创建工程 ① 启动Spring Tool Suite 4,指定工作目录。选择菜单【File → New → Spring Starter Project】出现如下画面(注意:系统不同,画面会稍有差异)。
② 点击Next按钮之后,如下图所示在搜索框内检索依存库的关键字,然后选中,选择工程依存关系。 本示例中使用的库有: - Web - Thymeleaf - MyBatis - Validation - H2 - DevTools。
③ 点击Finish按钮后,初始的程序框架就会自动生成,确认一下pom.xml文件内容就可以知道,上述的各种依存关系已经全部生成了。开发过程中需要新的依存,可以直接修改这个文件。pom.xml就是maven工程的灵魂。
(2) 创建模型(Model) 首先新建一个叫做Item的类(package:com.example.demo.domain), 定义商品的各种属性,并对各种属性进行检查。Spring Boot最大的特点就是利用各种annotation进行配置,至于后面的处理Spring Boot已经全给我们做了。
1 package com.example.demo.domain; 2 3 import javax.validation.constraints.Max; 4 import javax.validation.constraints.Min; 5 import javax.validation.constraints.NotBlank; 6 import javax.validation.constraints.Size; 7 8 public class Item { 9 private Long id;10 11 @NotBlank (message='商品名不能为空' )12 private String name;13 14 @Min (value=10 , message='不能小于10' )15 @Max (value=10000 , message='不能大于10000' )16 private float price;17 18 @Size (max=50 , message='生产商长度不能超过50' )19 private String vendor;20 21 public Long getId () {22 return id;23 }24 25 public void setId (Long id) {26 this .id = id;27 }28 29 public String getName () {30 return name;31 }32 33 public void setName (String name) {34 this .name = name;35 }36 37 public float getPrice () {38 return price;39 }40 41 public void setPrice (float price) {42 this .price = price;43 }44 45 public String getVendor () {46 return vendor;47 }48 49 public void setVendor (String vendor) {50 this .vendor = vendor;51 }52 }
(3) 创建视图(View)相关文件 ① 把前面下载的Bootstrap4及jQuery解压并找到相关文件放入resources/static目录下。 └static └css └bootstrap.min.css └js └bootstrap.min.js └jquery-3.4.1.min.js
② 在resources/templates下创建4个画面模版文件(html)。模版采用了Thymeleaf及Bootstrap,关于Thymeleaf及Bootstrap会在后续文章中说明。
1 <!DOCTYPE html> 2 <html > 3 <head > 4 <meta charset ='UTF-8' > 5 <meta name ='viewport' content ='width=device-width, initial-scale=1' > 6 <link rel ='stylesheet' href ='/css/bootstrap.min.css' /> 7 <title > 商品列表</title > 8 </head > 9 <body > 10 <nav class ='navbar navbar-inverse' > 11 <div class ='container' > 12 <div class ='navbar-header' > 13 <a class ='navbar-brand' href ='/items' > 商品管理DEMO</a > 14 </div > 15 </div > 16 </nav > 17 <div class ='container' > 18 <div class ='card card-primary mb-3' > 19 <div class ='card-header' > 20 <h5 class ='card-title' > 商品列表<a href ='/items/new' class ='btn btn-success float-right' > 新建</a > </h5 > 21 </div > 22 <div class ='card-body' th:if ='!${items.size()}' > 23 <p > 目前没有商品</p > 24 </div > 25 <table class ='table table-striped' th:if ='${items.size()}' > 26 <thead > 27 <tr > 28 <th style ='width: 10%' > ID</th > 29 <th style ='width: 30%' > 商品名</th > 30 <th style ='width: 10%' > 价格</th > 31 <th style ='width: 20%' > 生产商</th > 32 <th style ='width: 30%' > </th > 33 </tr > 34 </thead > 35 <tbody > 36 <tr th:each ='item:${items}' th:object ='${item}' > 37 <td th:text ='*{id}' > </td > 38 <td th:text ='*{name}' > </td > 39 <td th:text ='*{price}' > </td > 40 <td th:text ='*{vendor}' > </td > 41 <td class ='float-right' > 42 <form th:action ='@{/items/{id}(id=*{id})}' th:method ='delete' > 43 <a class ='btn btn-primary' th:href ='@{/items/{id}(id=*{id})}' > 详细</a > 44 <a class ='btn btn-primary' th:href ='@{/items/{id}/edit(id=*{id})}' > 变更</a > 45 <button class ='btn btn-primary' > 删除</button > 46 </form > 47 </td > 48 </tr > 49 </tbody > 50 </table > 51 </div > 52 </div > 53 <script src ='/js/jquery-3.4.1.min' > </script > 54 <script src ='/js/bootstrap.min.js' > </script > 55 </body > 56 </html >
1 <!DOCTYPE html> 2 <html > 3 <head > 4 <meta charset ='UTF-8' > 5 <meta name ='viewport' content ='width=device-width, initial-scale=1' > 6 <link rel ='stylesheet' href ='/css/bootstrap.min.css' /> 7 <title > 商品新建</title > 8 </head > 9 <body > 10 <nav class ='navbar navbar-inverse' > 11 <div class ='container' > 12 <div class ='navbar-header' > 13 <a class ='navbar-brand' href ='/items' > 商品管理DEMO</a > 14 </div > 15 </div > 16 </nav > 17 <div class ='container' > 18 <div class ='card card-primary mb-3' > 19 <div class ='card-header' > 20 <h5 class ='card-title' > 商品新建</h5 > 21 </div > 22 <div class ='card-body' > 23 <form th:method ='post' th:action ='@{/items}' th:object ='${item}' > 24 <div class ='form-group row' > 25 <label class ='col-md-2 control-label' > 商品名</label > 26 <div class ='col-md-10' > 27 <input class ='form-control' type ='text' name ='name' th:value ='*{name}' /> 28 <div class ='text-danger' th:if ='${#fields.hasErrors('name')}' th:errors ='*{name}' > </div > 29 </div > 30 </div > 31 <div class ='form-group row' > 32 <label class ='col-md-2 control-label' > 价格</label > 33 <div class ='col-md-10' > 34 <input class ='form-control' type ='text' name ='price' th:value ='*{price}' /> 35 <div class ='text-danger' th:if ='${#fields.hasErrors('price')}' th:errors ='*{price}' > </div > 36 </div > 37 </div > 38 <div class ='form-group row' > 39 <label class ='col-md-2 control-label' > 生产商</label > 40 <div class ='col-md-10' > 41 <input class ='form-control' type ='text' name ='vendor' th:value ='*{vendor}' /> 42 <div class ='text-danger' th:if ='${#fields.hasErrors('vendor')}' th:errors ='*{vendor}' > </div > 43 </div > 44 </div > 45 <div class ='form-group row' > 46 <div class ='offset-md-2 col-md-10' > 47 <button class ='btn btn-primary' > 新建</button > 48 </div > 49 </div > 50 </form > 51 </div > 52 </div > 53 </div > 54 <script src ='/js/jquery-3.4.1.min' > </script > 55 <script src ='/js/bootstrap.min.js' > </script > 56 </body > 57 </html >
1 <!DOCTYPE html> 2 <html > 3 <head > 4 <meta charset ='UTF-8' > 5 <meta name ='viewport' content ='width=device-width, initial-scale=1' > 6 <link rel ='stylesheet' href ='/css/bootstrap.min.css' /> 7 <title > 商品详细</title > 8 </head > 9 <body > 10 <nav class ='navbar navbar-inverse' > 11 <div class ='container' > 12 <div class ='navbar-header' > 13 <a class ='navbar-brand' href ='/items' > 商品管理DEMO</a > 14 </div > 15 </div > 16 </nav > 17 <div class ='container' > 18 <div class ='card card-primary mb-3' > 19 <div class ='card-header' > 20 <h5 class ='card-title' > 商品详细</h5 > 21 </div > 22 <div class ='card-body' > 23 <div th:object ='${item}' > 24 <div class ='form-group row' > 25 <label class ='col-md-2' > 商品名</label > 26 <div class ='col-md-10' th:text ='*{name}' > </div > 27 </div > 28 <div class ='form-group row' > 29 <label class ='col-md-2 control-label' > 价格</label > 30 <div class ='col-md-10 form-control-static' th:text ='*{price}' > </div > 31 </div > 32 <div class ='form-group row' > 33 <label class ='col-md-2 control-label' > 生产商</label > 34 <div class ='col-md-10 form-control-static' th:text ='*{vendor}' > </div > 35 </div > 36 </div > 37 </div > 38 </div > 39 </div > 40 <script src ='/js/jquery-3.4.1.min' > </script > 41 <script src ='/js/bootstrap.min.js' > </script > 42 </body > 43 </html >
1 <!DOCTYPE html> 2 <html > 3 <head > 4 <meta charset ='UTF-8' > 5 <meta name ='viewport' content ='width=device-width, initial-scale=1' > 6 <link rel ='stylesheet' href ='/css/bootstrap.min.css' /> 7 <title > 商品変更</title > 8 </head > 9 <body > 10 <nav class ='navbar navbar-inverse' > 11 <div class ='container' > 12 <div class ='navbar-header' > 13 <a class ='navbar-brand' href ='/items' > 商品管理DEMO</a > 14 </div > 15 </div > 16 </nav > 17 <div class ='container' > 18 <div class ='card card-primary mb-3' > 19 <div class ='card-header' > 20 <h5 class ='card-title' > 商品详细</h5 > 21 </div > 22 <div class ='card-body' > 23 <form th:action ='@{/items/{id}(id=*{id})}' th:method ='put' th:object ='${item}' > 24 <div class ='form-group row' > 25 <label class ='col-md-2 control-label' > 商品名</label > 26 <div class ='col-md-10' > 27 <input class ='form-control' type ='text' th:field ='*{name}' /> 28 <div class ='text-danger' th:if ='${#fields.hasErrors('name')}' th:errors ='*{name}' > </div > 29 </div > 30 </div > 31 <div class ='form-group row' > 32 <label class ='col-md-2 control-label' > 价格</label > 33 <div class ='col-md-10' > 34 <input class ='form-control' type ='text' th:field ='*{price}' /> 35 <div class ='text-danger' th:if ='${#fields.hasErrors('price')}' th:errors ='*{price}' > </div > 36 </div > 37 </div > 38 <div class ='form-group row' > 39 <label class ='col-md-2 control-label' > 生产商</label > 40 <div class ='col-md-10' > 41 <input class ='form-control' type ='text' th:field ='*{vendor}' /> 42 <div class ='text-danger' th:if ='${#fields.hasErrors('vendor')}' th:errors ='*{vendor}' > </div > 43 </div > 44 </div > 45 <div class ='form-group row' > 46 <div class ='offset-md-2 col-md-9' > 47 <button class ='btn btn-primary' > 更新</button > 48 </div > 49 </div > 50 </div > 51 </form > 52 </div > 53 </div > 54 <script src ='/js/jquery-3.4.1.min' > </script > 55 <script src ='/js/bootstrap.min.js' > </script > 56 </body > 57 </html >
(4) DB准备及MyBatis Mapper创建 ① 在resources目录下做成DB表生成文件,每次启动程序前会自动做成表。表结构如下。
No. 字段名 类型 长度 说明 1 id bigint 商品ID 2 name varchar 255 商品名 3 price real 价格 4 vendor varchar 255 生产商
1 CREATE TABLE IF NOT EXISTS item (2 id bigint (20 ) NOT NULL AUTO_INCREMENT,3 name varchar (255 ),4 price real ,5 vendor varchar (255 ),6 PRIMARY KEY (id ),7 ) ENGINE =InnoDB DEFAULT CHARSET =utf8;
② Spring Framework中DB数据处理有JPA等框架,虽然具有自动处理功能,但是在实际的项目开发中很难实现比较复杂的要求。现实开发中采用MyBatis的框架比较多。采用MyBatis最大的好处就是,可以直接把SQL语句和Java程序对应起来,可以灵活方便的实现各种复杂的数据库操作。 首先新建一个叫做ItemMapper的接口(package:com.example.demo.mapper), 注意不是class而是ineterface,该类中定义了5个方法,分别用于以下处理。那么只定义接口,具体实现在哪里做呢,答案是只要在接口定义的前面加上@Mapper,其它的事情框架给我们做了,我们甚至都可以不用关心。
No. 方法名 说明 1 findAll 获取所有商品列表 2 findOne 根据ID获取一种商品 3 save 保存商品 4 update 更新商品 5 delete 商品删除
1 package com.example.demo.mapper; 2 3 import java.util.List; 4 5 import org.apache.ibatis.annotations.Mapper; 6 7 import com.example.demo.domain.Item; 8 9 @Mapper 10 public interface ItemMapper {11 List<Item> findAll () ;12 13 Item findOne (Long id) ;14 15 void save (Item item) ;16 17 void update (Item item) ;18 19 void delete (Long id) ;20 }
③ 在com.example.demo.mapper下面新建对应SQL定义文件ItemMapper.xml,该文件就是对应上述每个方法的SQL语句。简单易懂,不再累述。
1 <?xml version='1.0' encoding='UTF-8'?> 2 <!DOCTYPE mapper PUBLIC '-////DTD Mapper 3.0//EN' 'http:///dtd/mybatis-3-mapper.dtd'> 3 <mapper namespace ='com.example.demo.mapper.ItemMapper' > 4 <select id ='findAll' resultType ='com.example.demo.domain.Item' > 5 select * from item 6 </select > 7 8 <select id ='findOne' resultType ='com.example.demo.domain.Item' > 9 select * from item where id= #{id}10 </select > 11 12 <insert id ='save' useGeneratedKeys ='true' keyProperty ='id' > 13 insert into item(name, price, vendor) values(#{name}, #{price}, #{vendor})14 </insert > 15 16 <update id ='update' > 17 update item set name=#{name}, price=#{price}, vendor=#{vendor} where id= #{id}18 </update > 19 20 <delete id ='delete' > 21 delete from item where id = #{id}22 </delete > 23 </mapper >
④ 新建一个DB访问的服务类ItemService(package:com.example.demo.service),service类主要定义事物处理,并把相关的功能总结成为部品,使程序看起来更清楚。下面的代码中,需要注意到两点,第一是@Service的定义,只要添加了这个定义,那么这个类就会自动注册成为bean,其它程序可以直接使用,上述程序中的@Mapper定义也是如此。另外一个是@Autowired,这个定义是就是引用bean,完全不需要像传统程序一样进行初始化处理。
1 package com .example .demo .service ; 2 3 import java .util .List ; 4 5 import org .springframework .beans .factory .annotation .Autowired ; 6 import org .springframework .stereotype .Service ; 7 import org .springframework .transaction .annotation .Transactional ; 8 9 import com .example .demo .domain .Item ;10 import com .example .demo .mapper .ItemMapper ;11 12 @Service 13 public class ItemService {14 15 @Autowired 16 private ItemMapper itemMapper;17 18 @Transactional 19 public List<Item> findAll() {20 return itemMapper .findAll ();21 }22 23 @Transactional 24 public Item findOne(Long id) {25 return itemMapper .findOne (id );26 }27 28 @Transactional 29 public void save(Item item) {30 itemMapper .save (item );31 }32 33 @Transactional 34 public void update(Item item) {35 itemMapper .update (item );36 }37 38 @Transactional 39 public void delete(Long id) {40 itemMapper .delete (id );41 }42 43 }
(5) 创建控制器(Controller) 新建一个ItemController的类(package:com.example.demo.controller),controller的主要作用就是从DB中取得数据,并交给View去渲染,或者从View中取得数据,在交给DB去保存。在类的定义前面加一个@Controller,各种配置就自动实现了,如果是开发REST API那么只要声明@RestController就可以。在这个类里面@AutoWired再次出现,就是将上述定义的servcie自动引入该类,而不需要各种初始化操作。@RequestMapping、@GetMapping、@PostMapping、@PutMapping等annotation用来定义HTTP请求URL和参数。
1 package com.example.demo.controller; 2 3 import org.springframework.beans.factory.annotation .Autowired; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.ui.Model; 6 import org.springframework.validation.BindingResult; 7 import org.springframework.validation.annotation .Validated; 8 import org.springframework.web.bind.annotation .DeleteMapping; 9 import org.springframework.web.bind.annotation .GetMapping;10 import org.springframework.web.bind.annotation .ModelAttribute;11 import org.springframework.web.bind.annotation .PathVariable;12 import org.springframework.web.bind.annotation .PostMapping;13 import org.springframework.web.bind.annotation .PutMapping;14 import org.springframework.web.bind.annotation .RequestMapping;15 16 import com.example.demo.domain.Item;17 import com.example.demo.service.ItemService;18 19 @Controller 20 @RequestMapping('/items' ) 21 public class ItemController {22 23 @Autowired 24 private ItemService itemService;25 26 @GetMapping 27 public String index(Model model) {28 model.addAttribute('items' , itemService.findAll());29 return 'index' ;30 }31 32 @GetMapping('{id}' ) 33 public String show(@PathVariable Long id, Model model) {34 model.addAttribute('item' , itemService.findOne(id));35 return 'show' ;36 }37 38 @GetMapping('new' ) 39 public String newItem(@ModelAttribute('item' ) Item item, Model model) {40 return 'new' ;41 }42 43 @GetMapping('{id}/edit' ) 44 public String edit(@PathVariable Long id, @ModelAttribute('item' ) Item item, Model model) {45 model.addAttribute('item' , itemService.findOne(id));46 return 'edit' ;47 }48 49 @PostMapping 50 public String create(@ModelAttribute('item' ) @Validated Item item, BindingResult result, Model model) {51 if (result.hasErrors()) {52 return 'new' ;53 } else {54 itemService.save(item);55 return 'redirect:/items' ;56 }57 }58 59 @PutMapping('{id}' ) 60 public String update(@PathVariable Long id, @ModelAttribute('item' ) @Validated Item item, BindingResult result, Model model) {61 if (result.hasErrors()) {62 model.addAttribute('item' , item);63 return 'edit' ;64 } else {65 item.setId(id);66 itemService.update(item);67 return 'redirect:/items' ;68 }69 }70 71 @DeleteMapping('{id}' ) 72 public String delete(@PathVariable Long id) {73 itemService.delete(id);74 return 'redirect:/items' ; 75 }76 }
(6) 启动测试 ① 至此我们就完成了一个简单的WEB程序,在IDE的Package Explorer里找到DemoApplication.java文件,该文件是工程自动做成的入口文件,如下图所示右键点击菜单中选择Spring Boot App,服务器就可以启动了。
②在浏览器中访问http://localhost:8080/items 就可以看到下面的页面了。
6. 结语 需要补充说明的是,一个现实中的的系统远远比上述示例复杂的多,比如认证处理、对话处理、数据物理存储、负荷分散等等等等。笔者认为只要基础有了,其它的在业务中很短时间就可以驾轻就熟。
在学习过程中发生调试不通的错误,示例代码可以从GitHub下载。https://github.com/lebo-itgo/springbootsample 另外,本文及示例的错误之处欢迎留言批评指正,您关心的话题也可以留言,我们会在30分钟系列里逐步推出相关文章。