SpringBoot模拟项目综合实践(活动创建-Hi现场)

模拟hi现场互联网项目的活动业务功能:hixianchang.com

这个软件上可以实现互动答疑功能,但是如果会议人数超过300人就需要收费:

1.首先点击登录,会弹出一个登录的模态框,这个模块框如何实现:

2.登录以后,点击创建活动,这个创建活动的按钮如何实现: 

3.活动的基本信息页面如何实现:

4.活动生效时间默认创建即生效,只允许选择活动失效时间,这个时间如何实现: 

5.活动类型为下拉选项,这个下拉选项如何实现:

6.基本信息输入完成,创建活动就会把活动信息写入数据库,这个功能如何实现:

一.搭建项目初始环境:

1.初始化数据库数据(命令行执行): 

1)登录Mysql客户端:mysql -uroot -proot
2)创建数据库dbactivity:create database dbactivity;或者create database if not exists dbactivity charset utf8;
3)查看数据库:show databases;
4)设置客户端编码:set names utf8;或者 set charset utf8;(如果要想识别utf8的文件,建议都是utf8的编码,一定要设置,因为导入sql文件的编码是utf8);
5)执行activity.sql文件,导入数据表文件activity.sql: source d:/activity.sql
6)打开数据库dbactivity:use dbactivity;
7)查看数据库的所有表:show tables;
8)查看数据库的某表结构:desc tb_activity;

2.创建SpringBoot Maven项目:

1) 官网访问超时:

 

 2)创建项目:

3)搜索、选择依赖(可能有些依赖是无法搜索到的,因为这里搜索的是spring官网提供的依赖库中的资源,如果资源库没有就无法搜索到):

注:在pom.xml文件中,如果第一次添加整合mybatis依赖的时候,由于spring官网资源库,默认是没有指定mybatis的版本,会有问题,需要自己指定一个,以后再次添加时sts会就会有版本记录,无需再次指定。

4)整合HikariCP连接池时,用的是MySQL Driver和JDBC API;整合MyBatis时,用的是MyBatis Framework;整合SpringMVC时,用的是Spring Web和Thymeleaf:

5)创建后的项目结构如下:后面会在stemplates目录里 再创建一个目录modules(当然其他名字也可以,与thymeleaf配置对应即可),模块目录。

注:如果在创建项目时,没有直接添加Spring Web和Thymeleaf,而是在已创建好的项目中引入这两个依赖,则需要自己手动构建目录结构,即在src/main/resources下添加static和templates这两个目录。

3.修改application.properties文件,进行资源配置:

1)添加数据源配置(使用内置的HikariCP连接池)

2)添加mybatis配置

3)添加thymeleaf配置

4)添加日志配置

代码如下:

#spring datasource

spring.datasource.url=jdbc:mysql:///dbgoods?serverTimezone=GMT%2B8&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root

#spring mybatis
mybatis.mapper-locations=classpath:/mapper/*/*.xml

#spring thymeleaf/spring web

spring.thymeleaf.prefix=classpath:/templates/modules/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false

#spring logging
logging.level.com.cy=debug

4.引入前端开发框架Bootstrap的CSS样式与JS脚本文件至项目结构

1)访问Bootstrap官网:https://www.bootcss.com/ 

2)点击Bootstrap3中文文档(v3.3.7),进入如下页面:

 

3)点击下载 Bootstrap,进入页面,在用于生产环境的Bootstrap,点击下载Bootstrap即可下载。

4) 将下载下来的 bootstrap-3.3.7-dist.zip 解压到一个目录中,然后把这个解压后的资源重命名去掉版本号,就命名为bootstrap,名字没有必要这么长:

5)这个bootstrap是别人写好的资源,我们需要用,所以就把bootstrap整个的这个目录,复制粘贴到项目src/main/resources目录下的static目录下:

注:bootstrap目录名尽量不要有什么版本信息,因为可能在html中引入资源时,可能需要对名字做额外的符号处理(比如下面官方文档的基本模板中版本号之前的@符号处理),这个目录下又包含css、fonts和js,这3个目录。

6)如何使用bootstrap,我们可以从官网https://www.bootcss.com/ 的Bootstrap3中文文档选项(我们下载是Bootstrap3版本):

7)在Bootstrap3中文文档中,上方有一个起步选项,它的下面有一个内容是 基本模板:

 

8)基本模板的内容是一段html模拟案例,其第一部分在head标签下,有link标签是对bootstrap.min.css的引入:

9)基本模板的内容的第二部分,在body区的最下方,有两个script标签,分别是对jquery.min.js和bootstrap.min.js的引入:

注:正如注释中所描述,jquery必须放到前面,因为bootstrap这个前端框架的js,它内部的一些函数是依赖于jquery的。

10)从Bootstrap官网中也可以见得。Bootstrap是依赖于jquery的:

11)引入jquery.min.js到项目目录src/main/resources/static下(jquery可以从官网 https://jquery.com/ 下载,或者用自己已经备份好的资源库找到):

 

12)在src/main/resources/static目录下,再创建一个目录jquery,然后把 jquery.min.js 拷贝到这个目录中:

 

注:bootstrap是从官网https://www.bootcss.com/ 上下载下来的,jquery也可以从官网去下载,但我们程序员都有自己的资源库,是之前下载好了的。

13)同时我们在templates下面创建一个目录modules模块(这里不写成pages,目录名可以自定义);然后在src/main/resources目录下,再创建一个文件夹mapper/activity,表示活动模块,后面会用来放springmvc配置的sql映射文件,结构如下:

5.引入前端开发框架Bootstrap的CSS样式与JS脚本文件至html页面

1)先以goods.html页面为例,根据Bootstrap官网的基本模板,引入Bootstrap前端开发框架,基本模板示例如下:

基本模板
使用以下给出的这份超级简单的 HTML 模版,或者修改这些实例。我们强烈建议你对这些实例按照自己的需求进行修改,而不要简单的复制、粘贴。
拷贝并粘贴下面给出的 HTML 代码,这就是一个最简单的 Bootstrap 页面了。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
      <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <h1>你好,世界!</h1>

    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </body>
</html>

2)根据模板示例, bootstrap.min.css 文件引入head标签里面:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet"> 改为:
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">//因为在项目static目录下,默认为项目根目录,端口号下面,所以以斜杠开头

 注:https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist,其实用这个地址也可以,这个地址是个网络地址,不过一旦这个网络不可访问的时候,此css就无法引入;

这里已经把Bootstrap拷贝到项目里面了,但将来项目上线以后,这个css也会放到一个专门的服务器上,比如cdn服务器,现在不考虑外网服务器,就放到自己项目内部即可。

3)根据模板示例, jquery.min.js 和 bootstrap.min.js 文件引入body标签里面,所有js建议放到body底部区域:

 <body>
    <h1>你好,世界!</h1>

    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </body>
同样,地址改为我们自己的项目地址:
    <script src="/jquery/jquery.min.js"></script>
    <script src="/bootstrap/js/bootstrap.min.js"></script>

4)根据官网教程使用Bootstrap,比如 全局 CSS 样式

 

4-1) 首先最上面概览里面介绍,如果是HTML5,建议这样写:

<!DOCTYPE html>
<html lang="zh-CN">
  ...
</html>

4-2) 还有移动设备优先,排版与连接,和布局容器:

4-3) 如果想移动设备优先,也可以做如下设置:

<meta name="viewport" content="width=device-width, initial-scale=1"> 或者:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

4-4) 比如布局容器:我们选择一个类选择器.container,一个容器应用在goods.html中试验。

.container 类用于固定宽度并支持响应式布局的容器。

<div class="container">
  ...
</div>
.container-fluid 类用于 100% 宽度,占据全部视口(viewport)的容器。

<div class="container-fluid">
  ...
</div>

4-5) 我们还想改一下表格的样式,比如我们在 https://v3.bootcss.com/css/#tables 找到表格,如果想要这种表格,就在table标签内加上如下属性:

<table class="table">
  ...
</table>

4-6) 我们还想改一下button的样式,比如我们在https://v3.bootcss.com/css/#buttons 找到按钮,我们要这个首选项是蓝色的这个:

<button type="button" class="btn btn-primary">(首选项)Primary</button>

4-7) 我们在goods.html中,body标签内部也加上一个div,将非script脚本标签的元素全部包在此div内部,然后div有一个属性class="container",一个容器,表示把这些内容写到一个容器里面,同时把表格table的样式,和 添加商品 按钮的样式,代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">

</head>
<body>
<div class="container">
 <h1>The Goods Page</h1>
 <button type="button" class="btn btn-primary" onclick="doLoadAddUI()">添加商品</button>
 <a th:href="@{/goods/doGoodsAddUI}">添加商品</a>
 <table class="table">
    <thead><!-- thead通常用于定义表格的title部分 -->
       <tr>
         <th>id</th> <!-- th用于定义当前列的标题 -->
         <th>name</th>
         <th>remark</th>
         <th>createdTime</th>
         <th colspan="2">operation</th>
       </tr>
    </thead>
    <tbody><!-- tbody中通常用于定义表格的表体部分:呈现具体业务数据的区域 -->
        <tr th:each="g:${list}">
          <td th:text="${g.id}">1</td>  
          <td th:text="${g.name}">MySQL</td>  
          <td th:text="${g.remark}">RDBMS</td>  
          <td th:text="${#dates.format(g.createdTime, 'yyyy/MM/dd HH:mm')}">2020/08/03 16:10</td> 
          <td><a th:href="@{/goods/doDeleteById/{id}(id=${g.id})}">delete</a></td> 
          <td><a th:href="@{/goods/doFindById/{id}(id=${g.id})}">update</a></td> 
        </tr>
    </tbody>
 </table>
</div>
 <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
 <script src="/jquery/jquery.min.js"></script>
 <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
 <script src="/bootstrap/js/bootstrap.min.js"></script>
 <!-- 建议:所有的js代码写到body的最底端 -->
 <script type="text/javascript">
      function doLoadAddUI(){
          //跳转到url对象的地址
          location.href='/goods/doGoodsAddUI';
          //location.href='doGoodsAddUI';
      }
 </script>
</body>
</html>

4-8)代码和效果图对比之在引入Bootstrap样式之前:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 <h1>The Goods Page</h1>
 <button type="button" onclick="doLoadAddUI()">添加商品</button>
 <a th:href="@{/goods/doGoodsAddUI}">添加商品</a>
 <table>
    <thead><!-- thead通常用于定义表格的title部分 -->
       <tr>
         <th>id</th> <!-- th用于定义当前列的标题 -->
         <th>name</th>
         <th>remark</th>
         <th>createdTime</th>
         <th colspan="2">operation</th>
       </tr>
    </thead>
    <tbody><!-- tbody中通常用于定义表格的表体部分:呈现具体业务数据的区域 -->
        <tr th:each="g:${list}">
          <td th:text="${g.id}">1</td>  
          <td th:text="${g.name}">MySQL</td>  
          <td th:text="${g.remark}">RDBMS</td>  
          <td th:text="${#dates.format(g.createdTime, 'yyyy/MM/dd HH:mm')}">2020/08/03 16:10</td> 
          <td><a th:href="@{/goods/doDeleteById/{id}(id=${g.id})}">delete</a></td> 
          <td><a th:href="@{/goods/doFindById/{id}(id=${g.id})}">update</a></td> 
        </tr>
    </tbody>
 </table>
 <!-- 建议:所有的js代码写到body的最底端 -->
 <script type="text/javascript">
      function doLoadAddUI(){
          //跳转到url对象的地址
          location.href='/goods/doGoodsAddUI';
          //location.href='doGoodsAddUI';
      }
 </script>
</body>
</html>

效果图:

4-9)效果图对比之在引入Bootstrap样式之后:

模拟hi现场互联网项目的活动业务功能:

程序思路设计:

 

 调用流程设计:

服务端实现:

第一步:定义pojo对象(com.cy.pj.activity.pojo.Activity)

第二步:定义ActivityDao接口及方法

第三步:定义ActivityService接口及实现类

第四步:定义ActivityController对象及url映射

客户端实现:

第一步:定义activity.html页面

第二步:通过Thymeleaf模板引擎将活动数据呈现在页面上。

程序代码实现:

1)activity表结构对象建模:在com.cy.pj.activity.pojo包下,创建实体类Activity.java,用于存储用户的活动信息,这个活动信息有可能是从数据库查询出来的,也有可能是在做保存操作的时候,用来接收页面端传递过来的数据。

2)对应数据库表结构,构建Activity对象属性,get/set及toString方法等:

3)在src/main/java/com/cy/activity/pojo包下,创建Activity.java,代码实现:

package com.cy.pj.activity.pojo;

import java.util.Date;

/**
 * 
 * @author Administrator
 *
 */
public class Activity {

    private Long id;
    /** 活动标题 */
    private String title;
    /** 活动类型 */
    private String category;
    /** 活动开始时间 */
    private Date startTime;//java.util.Date
    /** 活动结束时间 */
    private Date endTime;
    /** 活动备注 */
    private String remark;
    /** 活动状态(已启动,已结束,...) */
    private Integer state;
    /** 创建用户:一般是登录用户 */
    private String createdUser;
    /** 创建时间:用户无需自己填写,有底层系统生成 */
    private Date createdTime;
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getCategory() {
        return category;
    }
    public void setCategory(String catagory) {
        this.category = catagory;
    }
    public Date getStartTime() {
        return startTime;
    }
    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }
    public Date getEndTime() {
        return endTime;
    }
    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public Integer getState() {
        return state;
    }
    public void setState(Integer state) {
        this.state = state;
    }
    public String getCreatedUser() {
        return createdUser;
    }
    public void setCreatedUser(String createdUser) {
        this.createdUser = createdUser;
    }
    public Date getCreatedTime() {
        return createdTime;
    }
    public void setCreatedTime(Date createdTime) {
        this.createdTime = createdTime;
    }
    @Override
    public String toString() {
        return "Activity [id=" + id + ", title=" + title + ", category=" + category + ", startTime=" + startTime
                + ", endTime=" + endTime + ", remark=" + remark + ", state=" + state + ", createdUser=" + createdUser
                + ", createdTime=" + createdTime + "]";
    }
        
}

根据表结构设计:

第一个字段id是bigint(20),表示活动ID,一般地,我们在对象中bigint对应字段为Long类型属性id,但我们非要定义成Integer也是可以的,没有问题;

第二个字段title是varchar(100),活动标题定义为String类型;

第三个字段category是varchar(100),一般地,类别可以定义为枚举类型,或者可以定义为字符串String,在页面上给定几个值去选择;

第四、五个字段是startTime和endTime是datetime,表示可以接收年月日时分秒,我们可以定义为Date(java.util)类型,java.sql.Date是在从数据库里面取数据时,做数据封装的时候会用,而在java程序中使用的类,就是java.util.Date,否则的话,程序会报错;

第六个字段remark是text,活动备注也可以定义为String类型;

第七个字段state是tinyint(4),数据库里设置的是tinyint小整形,当然如果这里只有两种状态的话,还可以设置为布尔类型,活动状态有效或无效,否则就需要设置为小整形,比如活动创建,已创建未启动,活动已经启动,活动已结束等。所以这里小整型在对象中对应的字段为Integer即可;

第八个字段createdUser是varchar(100),表示哪一个用户创建的活动,一般就是指登录用户,对应字段类型为String;

第九个字段createdTime是datetime,表示创建时间,无需用户自己填写,由底层系统生成,对象中用java.util.Date

 一般地,我们从数据库查询出来的数据,要存储到这个对象中,默认是先调set方法,如果是把数据写入数据库,写在sql当中,是调用get方法来取值,所以生成set/get方法,一般地,为了输出方便,还会重新toString方法。

4)根据程序设计,我们需要做查询,把数据库里的数据查询出来,我们还需要一个Dao层里面,去实现一下这个过程,那就得有一个Dao对象,一个接口;按照我们的结构来讲,它应该在src/main/java/com/cy/pj/activity/dao包下,创建一个接口ActivityDao.java:

说明:在这个ActivityDao当中,第一步,我们可能由MyBatis底层来给这个接口产生一个实现类,习惯地,在数据层的接口之上加一个@Mapper注解,然后在接口内部定义一个方法,其返回值是List<Activity>,方法名为findActivities();,引入java.util.List集合包,通过@Select注解写一个查询映射,@Select("select * from tb_activity order by createdTime desc"),此处简写以星号代表查询所有(实际工作时不建议写星号),最后记得写文档注释,查询所有活动信息,返回值就是活动信息。

注意:

* 在后续在查询时,可能会做一些调整,不一定是直接返回一个List集合,可能还会做一个分页的查询等多种设计,也有人会在List集合里面放一个Map,List<Map<String,Object>>,如果是这种形式的话,那就是一行记录一个Map对象,这时pojo都可以不写了;我们现在这里用的是pojo对象来封装,即一行记录,

映射为内存中的一个pojo对象。

* 这里还需要注意的是,我们只写了接口,那么没有实现类,是谁负责与数据库进行交互,当然这里其实是SqlSession,虽然我们没有自己去用它,但在Mybatis底层的实现类里面,肯定是SqlSession与数据库交互的,更具体一点,其实就是Connection,再具体一点就是JDBC API,即底层会通过JDBC API去实现服务器与数据库的交互。

* 那谁负责将ResultSet结果集中的数据取出来存储到Activity,我们说是MyBatis,再具体一点,那是MyBatis中哪一个对象去处理呢,就是ResultSetHandler,这是Mybatis里面的一个对象,我们数据库里面是一行一行的记录,那把这一行数据取出来的是谁呢,这肯定是通过SqlSession与数据库进行会话的,取出来的结果可能要存储到Activity对象,那谁去帮我存储呢,就是ResultSetHandler,这属于Mybatis的API,Ctrl+Shift+T打开ResultSetHandler,它是一个接口,里面有三个方法,其中List<E> handleResultSets(Statement stmt)是处理结果集的,首先这个方法返回的为什么是List集合啊?那注意我们这里的pojo传的是什么,那么这个List<E>集合,指定的泛型就是什么;那方法的参数是Statement,这个Statement的作用是负责发送sql的,执行sql还是在数据库端,Statement仅仅相当于是一个传送器,把sql发送到数据库端,假如是查询sql,Statement会获取一个ResultSet对象,引入结果集;那么handleResultSets方法的内部会做什么事情呢,就是把结果集中的数据取出来,取出来以后,最终存储到一个List集合中,当然ResultSetHandler仅仅是一个接口,如果想找到接口的实现类,选中此接口名称,Ctrl+T,就可以看到它的实现类为DefaultResultSetHandler,即结果集的处理。有的时候,我们从数据库取数据了,取出来以后,那它怎么就存到对象里面了么,肯定是有人负责去把数据取出来存到对象,那我们就可以找到这个ResultSetHandler的实现类DefaultResultSetHandler,在这个实现类里面,Ctrl+O,找到那个List<Object> handleResultSets(Statement stmt)方法:

  //
  // HANDLE RESULT SETS
  //
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

其中,handleResultSet(rsw, resultMap, multipleResults, null);,就是处理结果集,这就是在处理结果集,取数据做映射,真正的结果映射就在这。

    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

可以找到handleResultSet方法,从而找到具体的那个结果集的映射:其中 handleRowValues 就是映射行,从这可以了解这个结果集的映射与存储。

  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

以上就是Mybatis处理结果集的源码,所谓处理结果集,

handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping)

就是把结果ResultSetWrapper取出来,可能会存储List<Object>这个List集合里面,就ok了。这就是为什么我们只写了一个ActivityDao接口的方法List<Activity> findActivitys(),那写完这个接口方法上面,我们定义了一个sql,其实系统底层肯定是要把这sql发送到我们的数据库端,数据库端执行这个sql,执行完以后,

可能会返回一个结果,那个结果就是一个ResultSet,然后谁从ResultSet里面把数据取出来,存储到List<Activity>,这么一个List集合里面,就是ResultSetHandler,结果集处理器:

package com.cy.pj.activity.dao;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;


import com.cy.pj.activity.pojo.Activity;

@Mapper
public interface ActivityDao {
     @Select("select * from tb_activity order by createdTime desc")
     List<Activity> findActivitys();
}

那数据层接口写好以后,那下一步,我们可能会写一个业务层接口ActivityService 和 它的实现类ActivityServiceImpl,以及它们分别所在的包com.cy.pj.activity.service和com.cy.pj.activity.service.impl:

先创建ActivityService接口,并在其中定义一个方法List<Activity> findActivitys();,这个方法是控制层与业务层连接的入口,即控制层通过接口调用业务层的这个方法,这个方法名与数据层接口ActivityDao中的那个方法是一样,都是List<Activity> findActivitys();,在引包的时候,引入某个包下的类,Ctrl+Shift+O,

自动引入,ActivityService,这是业务层接口。

package com.cy.pj.activity.service;
import java.util.List;
import com.cy.pj.activity.pojo.Activity;
//引入包中的类:ctrl+shift+o

public interface ActivityService {
    List<Activity> findActivitys();
}

那这个ActivityService接口下面,我们还会写上一个实现类,在com.cy.pj.activity.service.impl包下,创建ActivityServiceImpl类去实现这个ActivityService接口,然后这个实现类根据设计,是需要交给Spring管理的,所以类上用@Service业务层注解描述,这个类可能要依赖于数据层对象,

并通过数据层对象去执行我们的数去查询,声明一个private ActivityDao activityDao;,并通过Spring给它注入一个值,所以属性上用@Autowired注解描述,其他的业务暂时先不写,直接在findActivitys()方法内调用它的数据层对象,return activityDao.findActivitys();,就看它的结构,以后的代码接口都基本如此形式,

后续的业务逻辑也都会写在这里,比如缓存功能,日志功能等;然后在控制层直接调用我们的业务层方法来拿到一个结果。

package com.cy.pj.activity.service.impl;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.cy.pj.activity.dao.ActivityDao;
import com.cy.pj.activity.pojo.Activity;
import com.cy.pj.activity.service.ActivityService;

@Service
public class ActivityServiceImpl implements ActivityService {

    @Autowired
    private ActivityDao activityDao;
    
    @Override
    public List<Activity> findActivitys() {
         return  activityDao.findActivitys();
    }
}

下一步是控制层,在com.cy.pj.activity.controller包下,创建ActivityController,首先这ActivityController也要交给Spring管理,所以通过@Controller注解描述,并让它耦合于业务层接口ActivityService,

然后通过注解@Autowired为它注入一个实现类,然后在其下再定义一个方法,public String doFindActivitys(),并希望其返回值为activity.html页面,请求映射通过注解@RequestMapping("/activity/doFindActivitys")描述,接下来,List<Activity> list = activityService.findActivitys();,调用业务层方法findActivitys(),

返回一个List<Activity>集合,先看看能不能拿到数据,数据的显示是拿到数据以后的操作,所以做一个输出,list.forEach((item)->System.out.println(item));,集合点forEach方法,这是jdk8的新特性,item是集合中的一个数据,然后输出这个item数据,forEach这就相当于通过for循环迭代这个List集合,其中,

-> 是一个箭头函数,在java中其实它是运算符,是jdk8对for循环的简化写法。for(int i=0; i<list.size(); i++){System.out.println(list.get(i));},或者 for(Activity item:list){System.out.println(item);},再或者新的写法,list.forEach(System.out::println);,这4中方式都是可以的,通过看源码可以看到别人在用的一些新特性,

比如jdk8中lambda表达式箭头函数,和jdk8中的双冒号方法引用,以及增强for循环。代码实现如下:

package com.cy.pj.activity.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.RequestMapping;

import com.cy.pj.activity.pojo.Activity;
import com.cy.pj.activity.service.ActivityService;

@Controller
public class ActivityController {
     @Autowired
     private ActivityService activityService;
          
     /**查询所有活动信息*/
     @RequestMapping("/activity/doFindActivitys")
     public String doFindActivitys(Model model) {
         List<Activity> list=activityService.findActivitys();
//         for(int i=0; i<list.size(); i++) {
//             System.out.println(list.get(i));
//         }
         
//         for(Activity item:list) {
//             System.out.println(item);
//         }
         
//         list.forEach((item)->System.out.println(item));//JDK8 lambda
         
         list.forEach(System.out::println);//JDK8 方法引用
         return "activity";
     }
}

最后在控制层的doFindActivitys是返回一个页面,这个页面还没有,接下来把这个页面先创建出来,在src/main/resources/templates/modules目录下,创建一个activity.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <h1>The Activity Page</h1>
</body>
</html>

页面也有了,然后启动服务器,我们一般在Boot Dashboard这个位置,通过选择项目右键(Re)start,去启动服务器,当在这启动服务的时候,在Console控制台这个位置,右上方有一个圈A,当点击这个A的时候,控制台在服务器启动时的输出将会以方框问号为分隔符,重新点中取消重启即可。

 访问localhost:8080/activity/doFindActivitys,将出现如下页面:

控制台输出如下:

下一步,通过BootStrap和thymeleaf将数据呈现在activity.html的页面上:

首先可以先把ActivityController这个类中的doFindActivitys方法中的打印测试语句先提取出来,不需要用了,Alt+Shift+M,提取到方法doPrint(list)中,通过这个方法封装这段代码打印一个集合,把集合里面的内容输出一下,不需要打印,就注释掉这个doPrint(list);即可:

 代码如下:

package com.cy.pj.activity.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.RequestMapping;

import com.cy.pj.activity.pojo.Activity;
import com.cy.pj.activity.service.ActivityService;

@Controller
public class ActivityController {
     @Autowired
     private ActivityService activityService;
          
     /**查询所有活动信息*/
     @RequestMapping("/activity/doFindActivitys")
     public String doFindActivitys(Model model) {
         List<Activity> list=activityService.findActivitys();
         //doPrint(list);
         return "activity";
     }

    private void doPrint(List<Activity> list) {//选中代码,快速提取方法,alt+shift+m
       //方法1:
//         for(int i=0; i<list.size(); i++) {
//             System.out.println(list.get(i));
//         }
           //方法2:
//         for(Activity item:list) {
//             System.out.println(item);
//         }
           //方法3:
//         list.forEach((item)->System.out.println(item));//JDK8 lambda
         //方法4:
         list.forEach(System.out::println);//JDK8 方法引用
    }
}

 那我们不是通过这个doPrint(list)方法把数据打印到控制台,我们要做的是把这个数据输出到activity.html页面上,下面我们就开始把activity.html的页面结构实现完整。

那么要做的是在这个页面上显示活动信息,为了获得这些活动数据,我们可以把ActivityController类的doFindActivitys方法里的,

         List<Activity> list=activityService.findActivitys();

这个地方获取的List集合,把它存储到一个Model对象中,因此为doFindActivitys添加一个Model参数,并通过这条语句,model.addAttribut("list", list);,将数据存到Model中,代码如下:

     /**查询所有活动信息*/
     @RequestMapping("/activity/doFindActivitys")
     public String doFindActivitys(Model model) {
         List<Activity> list=activityService.findActivitys();
         //doPrint(list);
       model.addAttribute("list", list); 
       return "activity";
   } 

那么在页面上如何去呈现这一部分数据呢,通常body标签内部,会分成几个div容器里面,我们把h1标签放到div里面,在h1下面再写一个table标签,在table里面,

我们写上表格的标题部分(表头),thead标签,在标题里面有tr和td或者th,和td相比,th标签的特点是标题内容会自动居中,当然,这个表格的标题部分,这块可以动态生成,即服务端返回了几列,在thead里面就创建几个td或th,服务端的List集合有多少行,我们这边的tr就动态的创建几个等。

我们这里有几列,即几个th,是根据Activity实体类进行相应的设置的,比如有title标题,有分类Category,或者说类型,有开始时间StartTime,有结束时间EndTime,有State状态,还有Remark备注,这个备注有的时候是不显示的,备注是查看详细信息时才显示的,这里干脆也不显示它了;至于Acitivity实例类中的创建用户createdUser和创建时间createdTime,这里就不写了,实际具体显示什么数据会由业务需求规定。最后再给上一个操作吧,后面可能也需要有一些操作,比如叫Operation。然后表格中具体的内容数据,我们显示在tbody里面,tbody中我们显示是tr,在tr内部,我们一般情况下,用的是这个属性去迭代我们的数据,th:each="${}",如果用thymeleaf的话,就直接<tr th:each="aty:${list}"> </tr>,在${list}内部写上list,并指定一个变量aty(activity),接收这个服务端Model传递过来的数据,然后在tr内部,我们对应表头写上相应的td标签,这个对应标题的td,我们写上<td th:text="${aty.title}"></td>,接着将其他从服务端响应过来的数据也响应的获取到,最后一个Operation操作所对应的td,我们写上一个按钮,关于开始时间StartTime和结束时间EndTime,一般的我们会按照自己的设计将其格式化,那这个格式呢,在thymeleaf里面,是在 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html,在第19章的Dates这个位置,就可以找到日期格式:

根据参考格式, ${#dates.format(date, 'dd/MMM/yyyy HH:mm')},我们写成

<td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>

,这里就不显示小时分钟了,实际这个是根据业务需求而定的,代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <div>
        <h1>The Activity Page</h1>
        <table>
            <thead>
                <tr>
                    <th>title</th>
                    <th>Category</th>
                    <th>StartTime</th>
                    <th>EndTime</th>
                    <th>State</th>
                    <th>Operation</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="aty:${list}">
                    <td th:text="${aty.title}"></td>
                    <td th:text="${aty.category}"></td>
                    <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${aty.state}"></td>
                    <td>Delete</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

把服务器启动起来,访问 http://localhost:8080/activity/doFindActivitys,访问效果如下:

那如果想把这个样式改的更加美观一点,就可以根据前述的goods商品模块,引入BootStrap前端框架对表格样式改善,之前已经把BootStrap和jquery的静态资源和脚本引入到这个activity活动模块中,src/main/resources/static目录下的jquery目录和bootstrap目录,有 jquery.min.js 和bootstrap相关的资源;

接下来就在activity.html页面引入这些资源(根据goods.html),第一个在head标签里引入bootstrap的css,第二个在body标签的底部引入jquery和bootstrap。然后样式根据goods.html对boostrap的引入,对于div来说可以用class="container",对于table这就class="table",首先是bootstrap对css的引用:

<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">

然后,bootstrap对js的引用:

 <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
 <script src="/jquery/jquery.min.js"></script>
 <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
 <script src="/bootstrap/js/bootstrap.min.js"></script>

 现在没有关系,但后面会利用js来操作一些页面上的对象时会用到,然后对于表格的最后一列Operation操作,我们可能会写上一个button表示delete操作,然后在table之上,我们也可能会写一个button表示添加活动操作,button的样式,我们可以可以分别设置为红色的,type="button" class="btn btn-danger",并且小一点的按钮,type="button" class="btn btn-danger btn-sm"和 type="button" class="btn btn-primary":

 

 

代码实现如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1>The Activity Page</h1>
        <button type="button" class="btn btn-primary">添加活动</button>
        <table class="table">
            <thead>
                <tr>
                    <th>title</th>
                    <th>Category</th>
                    <th>StartTime</th>
                    <th>EndTime</th>
                    <th>State</th>
                    <th>Operation</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="aty:${list}">
                    <td th:text="${aty.title}"></td>
                    <td th:text="${aty.category}"></td>
                    <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${aty.state==1?'有效':'无效'}"></td>
                    <td><button type="button" class="btn btn-danger btn-sm">delete</button></td>
                </tr>
            </tbody>
        </table>
    </div>
    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="/jquery/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

 访问 http://localhost:8080/activity/doFindActivitys 效果图如下:

 上面State,是一个数字1,数据库里保存的是1和0,但是这里想让它显示的是状态1-有效 或 0-无效,所以可以把这一列改为条件表达式,或者说三目运算符,在thymeleaf中同样支持:<td th:text="${aty.state==1?'有效':'无效'}"></td>,这个状态有效无效,指的是超时了,超过了活动结束时间,我们就需要把这个状态给改了,这个怎么改呢,可能要写一个任务调度,任务调度指的是,比如说我们在创建这条活动记录的时候,我们就要启动一个线程,这个线程就要对这个活动进行监控,一旦达到了比如到达活动结束时间以后,我们就去修改这个数据库中的状态state,把这个state的值改成 0-无效。这个不是手动去改,而是随着我们这个时间的滚动,它是动态更新的,当数据库里面的这个数据改了以后,在页面刷新时也会随之改动为有效或无效,这种情况下就是典型的任务调度了。这个任务调度可以使用线程池,也可以使用java当中的Timer类,就类似于我们在网上买了一个商品,下了一个订单,但这个订单30分钟没有付钱,或者20分钟没有付钱,订单自动取消,这个自动取消不是人工去取消的,而是我们这个订单到了一定的时间点以后,还没有被支付,它就取消了。那我们这里面创建的这种活动,后面做添加活动时也是一样,当在点击添加活动时,把活动信息写入数据库了,那就要考虑一个问题,什么时候这个数据库里面活动信息的状态会改成无效呢,不需要自己手动去改,那就得开启一个任务调度。

那这种情况下该怎么去启动这个任务调度,实现这个业务功能呢,如果实现了这个过程,就会意识到那个电商里面的恶意订单就是下了订单不付钱,或者在12306上订个票不支付,那同样,这个订单到点以后,不支付会自动取消,那么这里面的活动的状态,我们计划也这么玩。那么到这,我们把服务端的数据已经存储到一定的作用域,在客户端进行呈现这个过程也完成了,然后又通过引入bootstrap.min.css,更改了样式,其次在body区底部又引入了jquery和bootstrap.min.js,即我们查询,页面的呈现,并且基于bootstrap去修改我们页面表格的样式,就都做完了。

 那下面我们要做的是添加活动这个功能操作,就是说我要把页面上的新增一个活动数据,要写到数据库里面,而且我们不打算再去写一个页面,这个可以在我们当前页面中去写,在当前页面中,要做的业务需求是,打开Bootstrap官网的JavaScript插件:

这里有一个模态框,什么叫模态框,简单来说就是弹出一个窗口,还有demo示例,根据这个demo示例。那我们希望在我们这个activity.html这个页面中,点击左上方的添加活动按钮时,弹出一个窗口,在窗口里面输出我们的活动信息,输出完以后关闭,下面的活动页面自动更新,就类似于在Hi现场那个页面上点击登录或注册时,弹出了一个框,这里也做这么一个业务。其实这种模态框的应用有很多很多场景,就类似于我们在sts中Ctrl+N,弹出一个窗口,也是模块框的一个场景,那我们如何去实现这个场景呢,其实对于Bootstrap来讲,它给出了这样一种形式,一个模态框的代码,它有标题部分,<div class="modal-header">,中间部分,<div class="modal-body">,最后一部分,<div class="modal-footer">,还有按钮,在最上方它也有一个按钮去触发模态框,那我们就可以替代我们自己写的按钮,<button type="button" class="btn btn-primary">添加活动</button>,因为这些bootstrap已经帮我们写好了,那我们Ctrl+CV即可:

 Bootstrap模态框示例demo:

<!-- Button trigger modal -->
<button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
  Launch demo modal
</button>

<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title" id="myModalLabel">Modal title</h4>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

我们将最上面按钮内部的 Launch demo modal 的名称改为 创建活动,那在当前的应用当中,点击创建活动按钮,它其实是怎么显示的这个模态框呢,这个模态框默认就有,只是它状态是隐藏状态,当我们在点击创建活动按钮时,这个按钮的属性上,它底层设置了这么一个开关,data-toggle="modal",这个开关的作用是什么呢,让我们这模态框,data-target="#myModal",给显示出来,这个按钮里的属性上的data-target="#myModal",这个data-target等于的id,它会和模态框里,即

<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">

触发模态框按钮的属性上 data-target="#myModal",会和这个模态框的div的属性里的id相同,data-toggle="modal" data-target="#myModal",就是让modal(这里的modal应该指class里的) 的id为myModal的模态框显示出来,模态框即是一个隐藏在网页中的窗口,那在这个窗口里面,标题部分,中间部分 和 底部的按钮,我们可以替换为自己的功能的内容,比如第一个标题部分,我们改为创建活动,<h4 class="modal-title" id="myModalLabel">创建活动</h4>;中间部分的div内部是我们要写的表单,<div class="modal-body"> ...</div>我们的表单可以放到这个div内部,所以我们要把我们添加活动的表单写到这个位置,那我们在Bootstrap官网 全局CSS样式 找到表单,选择其中水平排列的表单:

 注:单独的表单长度都设置为100%,如下:

demo示例代码:

<form class="form-horizontal">
  <div class="form-group">
    <label for="inputEmail3" class="col-sm-2 control-label">Email</label>
    <div class="col-sm-10">
      <input type="email" class="form-control" id="inputEmail3" placeholder="Email">
    </div>
  </div>
  <div class="form-group">
    <label for="inputPassword3" class="col-sm-2 control-label">Password</label>
    <div class="col-sm-10">
      <input type="password" class="form-control" id="inputPassword3" placeholder="Password">
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <div class="checkbox">
        <label>
          <input type="checkbox"> Remember me
        </label>
      </div>
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <button type="submit" class="btn btn-default">Sign in</button>
    </div>
  </div>
</form>

将以上代码放到模态框的body部分,但是其中的Remeber me记住我,和底下的button,Sign in也不需要,所以把它们删掉,因为模态框里面有按钮,所以按钮也不需要,activity.html页面代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1>The Activity Page</h1>
        <!-- Button trigger modal -->
        <button type="button" class="btn btn-primary btn-lg"
            data-toggle="modal" data-target="#myModal">创建活动</button>

        <!-- Modal(模态框) -->
        <div class="modal fade" id="myModal" tabindex="-1" role="dialog"
            aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <!-- 标题部分 -->
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal"
                            aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                        <h4 class="modal-title" id="myModalLabel">创建活动</h4>
                    </div>
                    <!-- body部分 -->
                    <div class="modal-body">
                        <form class="form-horizontal">
                            <div class="form-group">
                                <label for="inputEmail3" class="col-sm-2 control-label">Email</label>
                                <div class="col-sm-10">
                                    <input type="email" class="form-control" id="inputEmail3"
                                        placeholder="Email">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="inputPassword3" class="col-sm-2 control-label">Password</label>
                                <div class="col-sm-10">
                                    <input type="password" class="form-control" id="inputPassword3"
                                        placeholder="Password">
                                </div>
                            </div>
                        </form>
                    </div>
                    <!-- 按钮部分 -->
                    <div class="modal-footer">
                        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary">Save
                            changes</button>
                    </div>
                </div>
            </div>
        </div>
        <table class="table">
            <thead>
                <tr>
                    <th>title</th>
                    <th>Category</th>
                    <th>StartTime</th>
                    <th>EndTime</th>
                    <th>State</th>
                    <th>Operation</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="aty:${list}">
                    <td th:text="${aty.title}"></td>
                    <td th:text="${aty.category}"></td>
                    <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${aty.state==1?'有效':'无效'}"></td>
                    <td><button type="button" class="btn btn-danger btn-sm">delete</button></td>
                </tr>
            </tbody>
        </table>
    </div>
    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="/jquery/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

页面效果图如下:

那接下来,就可以根据活动表中的数据,去构建这个创建活动的模态框页面,另外,至于这个模态框到底拷贝到哪里,我们现在放到了table之上,那把模态框放到table之下也无所谓,甚至放到table所在div的外面也是可以的。下面我们把模态框的body部分也改一下,

                    <!-- body部分 -->
                    <div class="modal-body">
                        <form class="form-horizontal">
                            <div class="form-group">
                                <label for="inputEmail3" class="col-sm-2 control-label">Email</label>
                                <div class="col-sm-10">
                                    <input type="email" class="form-control" id="inputEmail3"
                                        placeholder="Email">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="inputPassword3" class="col-sm-2 control-label">Password</label>
                                <div class="col-sm-10">
                                    <input type="password" class="form-control" id="inputPassword3"
                                        placeholder="Password">
                                </div>
                            </div>
                        </form>
                    </div>

其中,<label for="inputEmail3" class="col-sm-2 control-label">Email</label>,这里我们就不写Email了,改为title;然后,<input type="email" class="form-control" id="inputEmail3" placeholder="Email">,一个title就是一个普通的文本,类型改为text即可,它的id呢,改为titleId,随之,label的for属性也要改为titleId,那为什么label的for属性要对应下面input的id属性呢,作用就是无论我们点击这个title的label标签时,光标停在什么位置,这里的作用就是点击这个title的label标签时,会定位或选中id为titleId的input表单控件元素;那我们要把表单提交的话,还需要在这个id为titleId的input里面定义一个name属性,比如name="title",这个name属性的作用,可以把这个name为title所对应的信息,可能要提交到我们的服务端,服务器端是拿这个name的值(这里为title)作为参数为依据取值的,那title对应的值就是要写到数据库里面的那个值:

后面这个 <div class="form-group">,可能是一个类型,比如说有一个分类categoryId,下面的不想用input了啊,想改成下拉选项<select id="categoryId" class="form-control">,它里面有一些选项<option>,比如教育培训,企业活动,交友活动等,那这些option后面的值是什么,即写到数据库时会有一些value,那这些value就是在option里面取定义的内容,value属性是写到数据库的内容,option标签体中的内容是显示出来的内容,那select里面可能还有一个name="category",用一个选择框实现这个控件,代码如下:

                    <!-- body部分 -->
                    <div class="modal-body">
                        <form class="form-horizontal">
                            <div class="form-group">
                                <label for="titleId" class="col-sm-2 control-label">title</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="title" id="titleId"
                                        placeholder="Email">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="categoryId" class="col-sm-2 control-label">类型</label>
                                <div class="col-sm-10">
                                      <select id="categoryId" name="category" class="form-control">
                                          <option value="教育培训">教育培训</option>
                                          <option value="企业活动">企业活动</option>
                                          <option value="交友活动">交友活动</option>
                                      </select>
                                </div>
                            </div>
                        </form>
                    </div>

那已经有了title和category,那我还需要有一个时间,创建时间和结束时间,即有效时长,那我想把两个日期写在一起,用如下的Bootstrap样式:

demo示例代码:

<form class="form-inline">
  <div class="form-group">
    <label class="sr-only" for="exampleInputEmail3">Email address</label>
    <input type="email" class="form-control" id="exampleInputEmail3" placeholder="Email">
  </div>
  <div class="form-group">
    <label class="sr-only" for="exampleInputPassword3">Password</label>
    <input type="password" class="form-control" id="exampleInputPassword3" placeholder="Password">
  </div>
  <div class="checkbox">
    <label>
      <input type="checkbox"> Remember me
    </label>
  </div>
  <button type="submit" class="btn btn-default">Sign in</button>
</form>

将其添加到我们的activity.html页面中,只取前两个div,将示例中的form,改为div,即这个div里面设置了一个class="form-inline",代码效果如下:

                            <div class="form-inline">
                              <div class="form-group">
                                <label class="sr-only" for="exampleInputEmail3">Email address</label>
                                <input type="email" class="form-control" id="exampleInputEmail3" placeholder="Email">
                              </div>
                              <div class="form-group">
                                <label class="sr-only" for="exampleInputPassword3">Password</label>
                                <input type="password" class="form-control" id="exampleInputPassword3" placeholder="Password">
                              </div>
                            </div>

 发现前面还应该有一个label标签更好,代码修改及效果图如下:

                            <div class="form-inline">
                                <label for="categoryId" class="col-sm-2 control-label">Category</label>                            
                                <input type="email" class="form-control" id="exampleInputEmail3" placeholder="Email">
                                <input type="password" class="form-control" id="exampleInputPassword3" placeholder="Password">
                            </div>

 发现效果并不好看,还不如写成两行的好,那复制第一个title的div进行相应的修改:

                            <div class="form-group">
                                <label for="startTimeId" class="col-sm-2 control-label">StartTime</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="startTime" id="startTimeId"
                                        placeholder="start time">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="endTimeId" class="col-sm-2 control-label">EndTime</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="endTime" id="endTimeId"
                                        placeholder="end time">
                                </div>
                            </div>

最后还有一个textarea,可以选择Bootstrap中的样式,我们可以仿照title标签,修改为textarea,并设置其行数rows="5":

 demo示例代码:<textareaclass="form-control"rows="3"></textarea>

代码如下:

                            <div class="form-group">
                                <label for="remarkId" class="col-sm-2 control-label">Remark</label>
                                <div class="col-sm-10">
                                    <textarea type="text" class="form-control" rows="5" name="remark" id="remarkId"                                        placeholder="remark">
                                    </textarea>
                                </div>
                            </div>

最终效果如下:

那下一步,我们模态框点击提交时是一个按钮button,但是这个按钮没有写到表单里面,当我们点击这个Save按钮的时候,我们可能要提交表单,把表单里面的数据写到数据库里面,我们并没有用form表单里面的button type="submit"的形式的按钮,而是用的form外面的一个按钮button,那想把我们的表单的数据写到我们的数据库里面,应该如何提交,如何去实现呢,那首先得找到这个表单,可以通过表单的名字去找到这个表单,现在我们的表单还没有名字,那就给它一个名字或者是起一个id,然后给form外部的button上加一个单击事件onclick="doSaveObject()",把数据保存一下,我们写一个方法,那这时我们点击这个button的时候,就会有doSaveObject()的一个事件处理函数,那么在这个事件处理函数里面,想办法把表单提交给服务器端即可。另外,在这个form表单的属性上,我们可能还要写上一个action,要跳转到一个地址,可以用thymeleaf模板的写法,th:action,如果没有用thymeleaf标签的话,直接写那个地址也是可以的,那我们用thymeleaf写法,比如我们想提交的地址是/activity/doSaveActivity,然后表单提交方式method="post",即

那这时在服务器端要做一个save操作,在控制层ActivityController类中,添加这样一个方法doSaveActivity,并让它返回到activity.html页面,那这里先不考虑后台服务器保存刷新的问题,在这方法里首先做一个输出,测试在客户端提交的数据在服务器端是否可以收到,如果拿到数据,就可以把这些数据写入数据库里面了,那服务端的这个地址有了,接下来就可以去处理客户端,在activity.html页面里面如何提交表单中的数据,

     @RequestMapping("/activity/doSaveActivity")

     public String doSaveActivity(Activity activity) {

         System.out.println("activity="+activity);//检查客户端提交的数据

       //... ...

         return "activity";

     }

在activity.html中模态框中的form表单数据,我没有用submit的表单控件,就是用了一个普通的button,这时如果想提交这个表单,就需要在这个button上定义一个单击事件函数,那现在就写下这个函数doSaveObject(),我们把函数写在body体里面的最底部,首先在函数里面想取到模态框中的那个form,我们可以在浏览器的F12控制台测试,输出一下如何获取到那个form,可以发现通过$(form)的方式,$(form)这个是jquery中基于标签获取对象的一种方式,是可以拿到的,确实是个对象,因为页面中现在只有一个form表单,拿到的对象里面也有submit这样的函数,因为button没有在form表单的内部,所以这里必须通过定义函数事件方式提交表单,否则如果只是一个普通的button是不会提交表单的:

客户端activity.html的doSaveObject函数:

    <script type="text/javascript">
       function doSaveObject(){
         //表单校验(可考虑使用正则表达式)
         //提交表单
         //$(form)基于标签名(例如这里的标签名form)称获取表单对象
         //submit为jquery中的一个对象函数,通过此函数可以提交表单.
          $("form").submit();//提交表单
       }
    </script>    

那下面我们试试,这样取到的对象能否提交表单给服务器,那首先在项目的ActivityController中的doSaveActivity方法的System.out.println("activity="+activity)这,加一个断点,然后(Re)debug Ctrl+Alt+Shift+B,D 启动,加断点的目的就想看看提交时,客户端的数据有没有提交到服务器端,服务器拿到没有:

访问 http://localhost:8080/activity/doFindActivitys 点击创建活动按钮,填上数据,比如创建活动,教育培训,时间,这个开始和结束时间,默认必须以yyyy/mm/dd的格式来写,服务器才能直接保存到数据库中,而且将来这两个时间肯定要校验的,不能说结束时间还小于开始时间,备注就写互动答疑,然后,点按钮Save Changes:

 可以看到断点在System.out.println("activity="+activity);这里,是接收到了客户端提交的数据的,那接下来,把它们写到数据库中就完成了。此时发现浏览器端返回了一个空页面,因为控制层里直接返回了页面,并没有刷新页面的数据,服务端还没有写那个把数据写到数据库,再查询,再回到这个页面的过程:

 那如果想回到那个查询数据的操作,可以return "redirect:/activity/doFindActivitys";,然后把那个断点取消:

客户端页面,activity.html代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1>The Activity Page</h1>
        <!-- Button trigger modal -->
        <button type="button" class="btn btn-primary btn-lg"
            data-toggle="modal" data-target="#myModal">创建活动</button>

        <!-- Modal(模态框) -->
        <div class="modal fade" id="myModal" tabindex="-1" role="dialog"
            aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <!-- 标题部分 -->
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal"
                            aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                        <h4 class="modal-title" id="myModalLabel">创建活动</h4>
                    </div>
                    <!-- body部分 -->
                    <div class="modal-body">
                        <form class="form-horizontal" th:action="@{/activity/doSaveActivity}" method="post">
                            <div class="form-group">
                                <label for="titleId" class="col-sm-2 control-label">title</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="title" id="titleId"
                                        placeholder="Email">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="categoryId" class="col-sm-2 control-label">类型</label>
                                <div class="col-sm-10">
                                      <select id="categoryId" name="category" class="form-control">
                                          <option value="教育培训">教育培训</option>
                                          <option value="企业活动">企业活动</option>
                                          <option value="交友活动">交友活动</option>
                                      </select>
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="startTimeId" class="col-sm-2 control-label">StartTime</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="startTime" id="startTimeId"
                                        placeholder="start time">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="endTimeId" class="col-sm-2 control-label">EndTime</label>
                                <div class="col-sm-10">
                                    <input type="text" class="form-control" name="endTime" id="endTimeId"
                                        placeholder="end time">
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="remarkId" class="col-sm-2 control-label">Remark</label>
                                <div class="col-sm-10">
                                    <textarea type="text" class="form-control" rows="5" name="remark" id="remarkId"                                        placeholder="remark">
                                    </textarea>
                                </div>
                            </div>                            
                        </form>
                    </div>
                    <!-- 按钮部分 -->
                    <div class="modal-footer">
                        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary" onclick="doSaveObject()">Save changes</button>
                    </div>
                </div>
            </div>
        </div>
        <table class="table">
            <thead>
                <tr>
                    <th>title</th>
                    <th>Category</th>
                    <th>StartTime</th>
                    <th>EndTime</th>
                    <th>State</th>
                    <th>Operation</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="aty:${list}">
                    <td th:text="${aty.title}"></td>
                    <td th:text="${aty.category}"></td>
                    <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
                    <td th:text="${aty.state==1?'有效':'无效'}"></td>
                    <td><button type="button" class="btn btn-danger btn-sm">delete</button></td>
                </tr>
            </tbody>
        </table>
    </div>
    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="/jquery/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="/bootstrap/js/bootstrap.min.js"></script>
    <script type="text/javascript">
       function doSaveObject(){
         //表单校验(可考虑使用正则表达式)
         //提交表单
         //$(form)基于标签名(例如这里的标签名form)称获取表单对象
         //submit为jquery中的一个对象函数,通过此函数可以提交表单.
          $("form").submit();//提交表单
       }
    </script>    
</body>
</html>

到此为止,那创建活动唯一剩下的过程,就是把数据写到数据就可以了,服务端我们一般是倒序来写,先写Dao层,然后Service层,最后写Controller层,那首先来到ActivityDao这个接口,定义一个insertObject方法,然后给这个方法传入一个Activity对象,返回值为int,即int insertObject(Activity activity);,

package com.cy.pj.activity.dao;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.cy.pj.activity.pojo.Activity;

@Mapper
public interface ActivityDao {
    
     int insertObject(Activity activity);
     /**
      *   查询所有活动信息,一行记录映射为内存中的一个Activity对象
      *  1)谁负责与数据库交互?(SqlSession,再具体一点就是JDBC API)
      *  2)谁负责将ResultSet结果集合中的数据取出来存储到Activity?(MyBatis,再具体一点就是ResultSetHandler)
      */
     @Select("select * from tb_activity order by createdTime desc")
     List<Activity> findActivitys();
}

然后我们把这个Sql语句写到Mapper.xml映射文件里面去,我们从官网或其他的项目中拷贝一个Mapper.xml文件,放到src/main/resources/mapper/activity目录下,命名为ActivityMapper.xml,命名空间namespace="com.cy.pj.activity.dao.ActivityDao",insert标签上的id对应ActivityDao接口中的方法名,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="com.cy.pj.activity.dao.ActivityDao">
  
     <insert id="insertObject">
         insert into tb_activity
         (title,category,startTime,endTime,remark,state,createdUser,createdTime) 
         values 
         (#{title},#{category},#{startTime},#{endTime},#{remark},#{state},#{createdUser},now())
     </insert>
     
  </mapper>

 这是Dao层的代码实现,接下来是业务层Service,找到ActivityService接口,在com.cy.pj.activity.service包下。我们在这个ActivityService接口里第一个方法,int saveActivity(Activity entity);

package com.cy.pj.activity.service;
import java.util.List;
import com.cy.pj.activity.pojo.Activity;
//引入包中的类:ctrl+shift+o

public interface ActivityService {
    
    int saveActivity(Activity entity);
    List<Activity> findActivitys();
}

然后在service实现类里,去实现这个saveActivity(Activity entity)方法:在将要创建的活动信息保存的到数据库,在把活动信息写到数据库里之后,我们还要做些什么,不是定时销毁,我们说的是将来我们这里要开启任务调度,比如说开启任务或者活动倒计时,这就类似于下订单,时间到了以后,不需要人手动,

去把这个活动禁用,也就是活动到了结束时间应该将其状态修改为禁用0-状态,即无效状态,而开启一个线程是公用一个tomcat线程,还是再开启一个线程,那一定是开启另一个线程,这就是业务操作,那如何实现这个业务功能,可以基于java官方的有两种方案,第一个利用Timer类实现;

第二个利用ScheduleExecutorService,它是一个线程池中的任务调度;第三种方案,借助第三方的任务调度框架Quartz,任务调度框架的作用就是定时闹钟功能;那我们这里的活动业务,活动时间到了以后,应该再去调用dao的方法,将活动状态由,1-有效,改为,0-禁用;

下面我们以Timer的方式为例,它是最简单的实现方案,虽然存在很多弊端,并不理想,但是至少我们写会了这种形式,是能解决问题的,先不去探究它的好与不好,至少我们是可以去解决的,比如类似的定时关机,定时任务取消等需求,都是可以用Timer去实现的,我们先new一个Timer出来,如下图所示:

 通过Timer可以点出一些它里面的方法,实现任务调度功能的这个Timer里面能够接收这个时间的,一个是毫秒数,还有一个Date,还有firstTime,还有执行多少多少次,很多反复执行的,方法如下(具体细节见jdk源码):

        public void schedule(TimerTask task, long delay) {
            if (delay < 0)
                throw new IllegalArgumentException("Negative delay.");
            sched(task, System.currentTimeMillis()+delay, 0);
        }

        public void schedule(TimerTask task, Date time) {
            sched(task, time.getTime(), 0);
        }

        public void schedule(TimerTask task, long delay, long period) {
            if (delay < 0)
                throw new IllegalArgumentException("Negative delay.");
            if (period <= 0)
                throw new IllegalArgumentException("Non-positive period.");
            sched(task, System.currentTimeMillis()+delay, -period);
        }

        public void schedule(TimerTask task, Date firstTime, long period) {
            if (period <= 0)
                throw new IllegalArgumentException("Non-positive period.");
            sched(task, firstTime.getTime(), -period);
        }

 我们这里写的还是比较简单的,

 public void schedule(TimerTask task, Date time){... }

这个函数第一个参数,就是这里面的任务是TimerTask,我们直接在参数上new一个TimerTask(){},即以匿名内部类的方式执行任务,这个TimerTasker为任务,TimerTasker对象里有一个run方法,就是开始执行任务;那第二个参数是Date类型的时间time,就是到此时间以后去执行TimerTask任务,entity.getEndTime(),获取活动结束时间,到此时间开始执行任务调度函数TimerTask,即按这个指定时间执行任务。这是构建一个Timer对象,这个Timer对象可以负责去执行一些任务,此对象通过内置一个线程和一个任务队列,其中线程是负责执行启动run方法执行我们的任务,而任务队列的作用是,只有一个线程,如果有多个任务,即多次调用timer.schedule(task,time)函数,一个线程,多个任务,不可能同时执行,那么Timer对象就会把这些任务存储到一个容器里面,一个队列里面,先来的任务先执行,它是按照这么一种规则设计的对象。它的使用分为两步:

第一步:构建Timer对象;

第二步:启动线程执行任务,其中任务的类型是TimerTask类型,当我们这个Timer对象中内置的线程获得了CPU以后,Timer对象就会自动调用这个任务对象TimerTask的run方法,然后,在指定的一个时间去执行这个任务。

        //开启活动倒计时(活动到了结束时间应该将其状态修改为0)
        //方案:(自己尝试)
        //1)Java 官方:
        //1.1)Timer
        //1.2)ScheduledExecutorService
        //2)借助第三方的任务调度框架(任务调度框架,quartz)
        //方案1:Timer应用
        //1.1构建Timer对象
        Timer timer=new Timer();//此对象可以负责去执行一些任务(这个对象内置一个线程和一个任务队列)
        //1.2启动线程执行任务
        timer.schedule(new TimerTask() {//TimerTask为任务
            @Override
            public void run() {//一旦调用此任务的线程获得了CPU就会执行这个任务的run方法
                System.out.println("updateState.threadName="+Thread.currentThread().getName());
                System.out.println("执行任务...");
                //activityDao.updateState(entity.getId());
            }
        }, entity.getEndTime());//按指定时间执行任务.

重启下服务,创建一个活动AAAA,教育培训,结束时间到10:35,马上就10:35了,等一下看看是否会执行,这里的时间取得的是当前所在服务器的系统时间:

查看控制台,是否在指定结束时间,输出执行任务,可以发现是可行的:

这就是按指定时间去执行任务,那我们就可以在TimerTask的run方法内部去修改活动的状态信息了,要修改状态,我们需要在ActivityDao中在定义一个方法updateState,这个方法的入参应该有个id,根据id去查询此活动的记录,并把它的这个状态修改为 0-禁用 即可:

     @Update("update tb_activity set state=0 where id=#{id}")
     int updateState(Long id);

当然,此方法也可以传两个值,一个id,一个状态值state,可以去修改基于此id的活动记录的状态为 0-禁用 或者 1-启用。

数据层的Dao定义好方法以后,接下我们就可以在业务层的实现类ActivityServiceImpl.java中,直接调用这个ActivityDao中的updateState方法(这个方法没有定义在ActivityService接口中),并为之传入我们所要保存的对象的id,那这里有一个问题,既然是基于我们所正在创建的活动的id去修改状态,那首先我们需要拿到这个id,而页面上不可能提交id过来,因为这个活动是第一次创建的这么一条记录,如果想拿到这条id,首先是需要把此记录写到数据库以后,才能取到此id,那如何取到此id,这里有这样的一种机制:

打开ActivityMapper.xml映射文件,我们保存活动的insert的sql语句就写在这个文件中,我们想获取这条写入到数据库表中的这个记录的id值,因为这个id是自增长的,如果id是自增长的,那么它在mapper映射文件中的insert标签里,有一个useGeneratedKeys属性,将其设置为true,就表示我要获取写入到数据库表中的主键值;然后把这个主键值再赋值给此sql语句对应的数据层ActivityDao接口中,与之关联的方法参数对象中的另一个属性,具体是这个对象的哪一个属性,则由keyProperty来指定;那具体是方法中的哪个参数对象,可以由parameterType来指定,如果方法中只有一个参数对象,则parameterType通常是可以省略的,代码如下:

1)ActivityMapper.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="com.cy.pj.activity.dao.ActivityDao">
  
     <insert id="insertObject"
             parameterType="com.cy.pj.activity.pojo.Activity"
             useGeneratedKeys="true"
             keyProperty="id">
         insert into tb_activity
         (title,category,startTime,endTime,remark,state,createdUser,createdTime) 
         values 
         (#{title},#{category},#{startTime},#{endTime},#{remark},#{state},#{createdUser},now())
     </insert>
     
  </mapper>

2)数据层Dao 接口ActivityDao.java中对应方法:

package com.cy.pj.activity.dao;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.cy.pj.activity.pojo.Activity;

@Mapper
public interface ActivityDao {
     //...
     int insertObject(Activity activity);
     //...
}

useGeneratedKeys:表示使用insert操作中生成的自增主键值(这里必须是自增主键);

keyProperty:表示将获得的自增主键值赋值给参数对象的指定属性(这里是id属性),至于具体赋值给哪一个属性,需要根据业务需求而定;

注:这个keyProperty所指定的属性,一定是parameterType所指定的参数对象中的一个属性(我们这里指定的是com.cy.pj.activity.pojo.Activity),SpringMVC底层可能调用的是这个参数对象中(比如Activity对象)的setId方法,获取到的自增主键的值,赋值给parameterType所指定的对象,如果此对象中没有相应set方法,也可能是直接给属性赋值,所以就直接可以说给parameterType所指定的参数对象的被keyProperty所指定的属性赋值,这里是给Activity对象中的id属性赋值。因为这个参数对象是从客户端向服务端传数据时,由SpringMVC帮我们创建的,这是SpringMVC的一个规则,SpringMVC创建完Activity对象以后,这个对象里面并没有id值,我们从客户端向服务端提交数据,我们并没有提交id值,但是在这条活动数据创建完以后,我想立即基于id去修改这条记录,比如在我们的ActivityServiceImpl当中,基于id去修改这条记录,禁用它的活动状态,但从页面提交的数据,在服务器做了封装成Activity对象,这个对象中并没有id,但是我想拿到insert操作,写入到数据库表中这条记录的id值,就可以采用这样的一种策略,返回自动主键功能。即在SpringMVC的Mapper映射文件中的insert标签上,首先通过useGeneratedKeys告诉SpringMVC我们要用一下这个自增主键的值,然后通过keyProperty告诉SpringMVC,我们还要在parameterType所指定的参数对象中的某一个属性上去用一下这个获取到的主键值,从而当我们的insert操作执行完毕,这个由parameterType所指定的这个由客户端传递过来的数据,在服务器做了封装的参数对象里面的id属性就有值了,因此我们就可以利用这个对象,在业务层基于它的id对其所对应的记录进行修改,

activityDao.updateState(entity.getId());

这样就实现了在创建活动的同时,获取活动记录的自增主键id值,从而在活动结束时间到达以后,对基于此id的活动记录的状态值进行修改的功能。代码实现如下:

    @Override
    public int saveActivity(Activity entity) {
        int rows=activityDao.insertObject(entity);
        System.out.println("saveActivity.threadName="+Thread.currentThread().getName());
        //??????
        //开启活动倒计时(活动到了结束时间应该将其状态修改为0)
        //方案:(自己尝试)
        //1)Java 官方:
        //1.1)Timer
        //1.2)ScheduledExecutorService
        //2)借助第三方的任务调度框架(任务调度框架,quartz)
        //方案1:Timer应用
        //1.1构建Timer对象
        Timer timer=new Timer();//此对象可以负责去执行一些任务(这个对象内置一个线程和一个任务队列)
        //1.2启动线程执行任务
        timer.schedule(new TimerTask() {//TimerTask为任务
            @Override
            public void run() {//一旦调用此任务的线程获得了CPU就会执行这个任务的run方法
                System.out.println("updateState.threadName="+Thread.currentThread().getName());
                System.out.println("执行任务...");
                activityDao.updateState(entity.getId());
            }
        }, entity.getEndTime());//按指定时间执行任务.
        return rows;
    }

 现在这个功能的实现,就非常类似于我们现在互联网中的恶意订单,下了订单就是不付钱,这种订单我们一般情况下会把它理解成是恶意订单,那么这种恶意订单如何取消,就是应用的任务调度,时间到了自动去取消。这个基于Timer类实现的任务调度就完成了,但是这种Timer的方式,它有一个弊端,首先我们要执行的任务是属于业务层的逻辑,另外,此Timer对象可以负责去执行一些任务,这个对象会内置一个线程和一个任务队列,也就是说这个Timer类内的执行run的方法所在的线程,和Timer自身所在方法的线程是不一样的。我们分别输出一下这两个线程:

    @Override
    public int saveActivity(Activity entity) {
        //...
        System.out.println("saveActivity.threadName="+Thread.currentThread().getName());
        //...
        Timer timer=new Timer();//此对象可以负责去执行一些任务(这个对象内置一个线程和一个任务队列)
        //1.2启动线程执行任务
        timer.schedule(new TimerTask() {//TimerTask为任务
            @Override
            public void run() {//一旦调用此任务的线程获得了CPU就会执行这个任务的run方法
                System.out.println("updateState.threadName="+Thread.currentThread().getName());
                //...
            }
        }, entity.getEndTime());//按指定时间执行任务.
        return rows;
    }

重启服务器,创建一个活动任务:

 时间到10:50时,看控制台输出信息如下:

通过控制台的输出,我们可以发现确实是两个不同的线程,第一个saveActivity.threadName=http-nio-8080-exec-6,这个线程是属于tomcat中的线程,而updateState.threadName=Timer-0,这个线程是Timer对象内置的线程,我们不可能让tomcat的线程一直在这阻塞着,因为tomcat线程还要处理新的请求,如果很多类似的任务都阻塞了tomcat线程,那么tomcat就没有办法去处理新的请求了,那现在我们刷新下活动页面,观察此活动的状态是否已经更改,我们可以发现状态已经被修改为无效:

那这个活动结束时间后自动修改活动状态的功能就实现完成的,但是还有一个问题,假设我们现在所有的人,比如说有一万人去同时创建活动,那这个Timer对象是不是会构建一万个,那这个Timer对象要构建一万个的话,那一万个Timer对象,就对应着一万个线程对象,一个线程对象占用多少内存,一般操作系统给它分配的,默认是1M内存,一个线程占用1M,那一万个线程就占用10个G的内存,可以想象,瞬间10个G的内存就没有了,就算服务器内存比较多,并发量再大一点,可能系统就直接崩溃了,我们说线程数比较少无所谓,但是线程数一旦比较多,因为操作系统一般给我们线程默认分配的,就是1M内存,所以线程这块,它是重量级的一个对象,占用资源比较多,创建线程比较耗时,那这时使用Timer类去实现任务调度就不合适了,因为Timer这里面存在一个弊端,即每次Timer它都会启动一个新的线程,那有人想那就只由一个线程去执行,把Timer对象拿出去,放到一个工具类中去写,就一个Timer去执行多个任务,那就会造成阻塞,因为一个线程多个任务,就会有大量的阻塞在里面,因为这些任务就会存到(Timer内置的)一个任务队列了。

所以一般会用到线程池,也就是Java官方给出的第二种方式,ScheduledExecutorService,内置一个线程池。ScheduledExecutorService这个对象里面,它内置一个线程池,这个池中的线程可以重复应用,那我们可以通过这个池中的线程来处理多个并发请求,这样的话,性能上至少会好一些,可能对于资源的利用来讲,也会比较好,我们既要保证高效,又要实现低耗,还要保证线程安全,这是我们在程序开发中所要重点突破的点,虽然这个活动案例比较小,但很多地方都会有这样的应用场景,麻雀虽小,五脏俱全。

还有第三种写法,借助第三方的任务调度框架quartz,这个任务调度框架里面也使用了线程池技术,线程池是任务调度的基础。

那对于Timer对象的简单的用法就已经会用了,但是在我们这个程序中,如果在Timer的run方法中(内置的线程里),执行完了这个更新 activityDao.updateState(entity.getId());,还要再加一句timer.cancel();,就退出这个任务调度,因为这个任务都已经执行完了,还让这个线程继续运行着做什么呢,没有必要了,执行结束以后,我们调用timer的cancel()方法,结束这个任务,此时线程就会退出,后续线程也会销毁。

        //方案1:Timer应用
        //1.1构建Timer对象
        Timer timer=new Timer();//此对象可以负责去执行一些任务(这个对象内置一个线程和一个任务队列)
        //1.2启动线程执行任务
        timer.schedule(new TimerTask() {//TimerTask为任务
            @Override
            public void run() {//一旦调用此任务的线程获得了CPU就会执行这个任务的run方法
                System.out.println("updateState.threadName="+Thread.currentThread().getName());
                System.out.println("执行任务...");
                activityDao.updateState(entity.getId());
                timer.cancel();//退出任务调度(后续线程也会销毁)
            }
        }, entity.getEndTime());//按指定时间执行任务.

那还有人会想,我不想把这段代码写在这个saveActivity(Activity entity),这个方法里面,想把这段代码,执行任务的这段内容提取出去,提到一个类里面去,然后我们在业务层的这个saveActivity方法中去调用提取到那个类里面的方法,如果还有其他模块也用到了任务调度,我不想每个类里面都去写任务调度的这段代码,那其实我们想把这段代码提出去,首先,我们在 activityDao.updateState(entity.getId());,这里我们可能需要传一个id,这个是肯定要传的,同时这个activityDao也要提出去,因为其他模块如果也要用到这个ActivityDao,那就需要把这个dao也注入过去,如果我们不考虑dao的通用性,我们可以这样做,专门去写一个工具类,或者一个具体的对象,我们可以把这个具体的对象放在com.cy.pj.common.task包下, 就是执行任务的一个包,或者写在com.cy.pj.activity.scheduled包下,然后在包下定义一个类TimerScheduledTask,基于Timer实现的任务调度类,那实际我们就可以把上述任务调度的代码提取出来写成一个方法,或者直接提取到我们写的这个TimerScheduledTask工具类就可以,如果写成一个方法,选中需要提取的代码,然后Alt+Shift+M,提取成private void extracted(Activity entity),这样的一个方法,但是我们这个ActivityDao只能在当前类中去使用;如果写成一个工具类TimerScheduledTask,在其中定义一个静态的方法public static void schedule(TimerTask task, Date time){...},这行任务,不要求有什么返回结果,在这个schedule方法中new一个Timer,开启一个任务,接下来我们就可以在Service层调用这个工具类的schedule方法,传入任务和时间,那如果推出这个任务,在TimerTask这个任务中也有cancel方法,我们可以让这个任务cancel();

但这样去写的话,代码量并没有减少,意义不大,所以还是不建议提取这工具类,但这种抽取工具类的方式,需要去理解:

        //方案1:Timer应用
        //1.1构建Timer对象
        //Timer timer=new Timer();//此对象可以负责去执行一些任务(这个对象内置一个线程和一个任务队列)
        //1.2启动线程执行任务
        /*
         * timer.schedule(new TimerTask() {//TimerTask为任务
         * 
         * @Override public void run() {//一旦调用此任务的线程获得了CPU就会执行这个任务的run方法
         * System.out.println("updateState.threadName="+Thread.currentThread().getName()
         * ); System.out.println("执行任务..."); activityDao.updateState(entity.getId());
         * timer.cancel();//退出任务调度(后续线程也会销毁) } }, entity.getEndTime());//按指定时间执行任务.
         */        
        TimerScheduledTask.schedule(new TimerTask() {
            @Override
            public void run() {
                activityDao.updateState(entity.getId());
                cancel();
            }
        }, entity.getEndTime());

TimerScheduledTask工具类:

package com.cy.pj.activity.scheduled;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerScheduledTask {
    public static void schedule(TimerTask task, Date date) {
        Timer timer = new Timer();
        timer.schedule(task, date);
    }
}

 那这个Timer类中的schedule方法的第一个参数TimerTask能不能用lamda表达式的形式去写呢,并不能,TimerTask是一个抽象类,不是一个接口,必须是函数式接口才可以写成Lamda表达式形式,因为TimerTask里面,它还有其他的方法,要是只有一个方法就可以直接Lamda表达式。

 那以上,我们把保存活动记录,并获得返回自动主键的sql语句写在了ActivityMapper.xml中,那还可以不写在xml里,比方说以注解的方式去做,但对于复杂的sql,我们还是建议写在xml中,这里我们注释掉ActivityMapper.xml中的这段sql,再以注解的方式去实现这个sql语句:

 那现在我们来到ActivityDao.java,这个接口中,在dao中的 int insertObject(Activity activity) 方法上面,以注解的方式去实现保存活动记录的功能,那我们直接在方法之上写@insert,然后把之前在ActivityMapper.xml中写好的sql语句直接复制过来:

     @Insert("insert into tb_activity\r\n" + 
             "(title,category,startTime,endTime,remark,state,createdUser,createdTime) \r\n" + 
             "values \r\n" + 
             "(#{title},#{category},#{startTime},#{endTime},#{remark},#{state},#{createdUser},now())")
     int insertObject(Activity activity);

 我们发现复制过来的sql,又是字符串拼接,又是换行符什么的,如果不想以这种换行的方式去写的话,也可以把它写成一行,那还有useGenenratedKeys和keyProperty,那这里也可以用@Options注解,同样的去声明它们,至于parameterType,参数类型就不用写了,因为在insertObject(Activity activity)的方法上面,已经指定了这个Activity参数类型了,keyProperty所指定的id,就赋值给了这个Activity参数对象:

package com.cy.pj.activity.dao;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.cy.pj.activity.pojo.Activity;

@Mapper
public interface ActivityDao {
     //...    
     @Insert("insert into tb_activity (title,category,startTime,endTime,remark,state,createdUser,createdTime) values (#{title},#{category},#{startTime},#{endTime},#{remark},#{state},#{createdUser},now())")
     @Options(useGeneratedKeys = true, keyProperty = "id")
     int insertObject(Activity activity);
     //...
}

重启服务,重新再创建一个活动,可以测试程序依然是可以正常运行的。

最后来到控制层ActivityController的doSaveActivity方法中,调用activityService.saveActivity(entity);方法,代码如下:

package com.cy.pj.activity.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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.cy.pj.activity.pojo.Activity;
import com.cy.pj.activity.service.ActivityService;

@Controller
public class ActivityController {
     @Autowired
     private ActivityService activityService;
     
     @RequestMapping("/activity/doSaveActivity")
     public String doSaveActivity(Activity activity) {
         System.out.println("activity="+activity);//检查客户端提交的数据
         activityService.saveActivity(activity);
         return "redirect:/activity/doFindActivitys";
     }
     
     /**查询所有活动信息*/
     @RequestMapping("/activity/doFindActivitys")
     public String doFindActivitys(Model model) {
         List<Activity> list=activityService.findActivitys();
         model.addAttribute("list", list);
         return "activity";
     }
}

 然后访问 http://localhost:8080/activity/doFindActivitys 点击创建活动按钮,在弹出窗口中填写如下数据,页面就多出一条我们添加的数据(时间的格式必须以yyyy/MM/dd的形式,不能写成yyyy-MM-dd形式,因为SpringMVC有一个默认的时间格式,后面或说),默认状态是无效,因为我们没有给这个表设置默认值,那如果在这种没有给默认值的情况下,希望在创建活动时,让状态为有效状态,要如何实现呢,那我们就可以在Activity.java这个pojo对象里面,给这个状态设置一个默认值,private Integer state=1;,默认值为1,我们创建活动默认就是有效状态,如果将来需要无效状态,就把数据里的那个状态字段的值改为0即可:

 那到这,创建活动基本的业务流程就已经做完了,后续我们还可以在页面表单提交之前,即点击button时,可以对这个表单的内容做校验,比如在activity.html页面中,在doSaveObject()方法中,$("form").submit();,即这个表单提交之前,做一些表单校验,具体的实现,我们可以通过js拿到表单中的内容,比如title不允许为空,时间格式不正确,这些都可以使用一些正则表达式,校验成功则允许提交表单,否则,使其无法提交;另外这个活动无需验证重复,因为这个活动每个用户都可以创建,它的创建时间和创建用户都不一样,面向的用户也不一样,所以这里不需要验证重复;还有如果时间格式不对的话,比如填写的时间为2020-08-06的形式,那么就会报如下400异常:

 400异常,表示参数的个数,类型或格式不正确,这里就是由于时间参数的格式不正确所引起的:

 

因为SpringMVC默认可以处理的时间格式是yyyy/MM/dd,这样的格式。那如果就想用yyyy-MM-dd的格式,那就得指定日期格式,那如何指定日期格式呢,我们需要在pojo这个类里面去指定,即在Activity.java中,比如startTime和endTime属性,可以这样写:

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date startTime;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date endTime;

注意,这是保存数据执行save操作时的格式设置,如果将来想把数据库里面的数据取出来,以另外一种时间格式去呈现,那还有另外的格式设置方式,现在设置的仅仅是客户端向服务端提交时候的那个数据格式,可以通过@DateTimeFormat注解在pojo中定义,即SprigMVC基于@DateTimeFormat指定的日期格式接收客户端提交的数据,假如没有指定格式,SpringMVC默认格式为yyyy/MM/dd,建议也用这种格式,格式中的位数必须与输入的实际个数对应。

对于格式问题的解决方案,一般可以由前端去锁定,不允许用户自己输入,只能通过日期框进行选择:

那我们这里也可以去做一个类似这样的功能,在我们的activity.html活动页面,创建活动的模态框里,点击开始时间时,也能够弹出一个日期选择框:

 那这个功能是如何实现的呢,用的就是基于Bootstrap前端框架扩展的第三方插件Bootstrap datepicker,见后面有实现;那到这里,保存操作的基本逻辑流程就 走通了,接下来再做一个删除功能:

 点击delete按钮,我们可能是基于所选记录的id进行删除,如果基于id进行删除,那我们在服务器端就需要在ActivityController中添加这么一个方法 doDeleteById,或者叫 doDeleteObject,然后这个方法需要一个入参id,这个id的类型最好用Long,因为我们的pojo类中根据数据库字段定义的就是Long类型,与其保持一致。然后我们,在方法体内部把id打印一下,然后是返回值,删除完成后,还可以再查询一下,重定向到查询的请求,我们可以先简单一下,因为这里以重定向的方式并不理想,这里也可以直接在页面上更新,后面再说,最后映射请求路径为:"/activity/doDeleteObject"。代码如下:

    @RequestMapping("/activity/doDeleteObject")
    public String doDeleteObject(Long id) {
        System.out.println("delete.id="+id);
        //...
        return "redirect:/activity/doFindActivitys";
    }

控制层先写到这里,我们在客户端做一些修改,看看能否在控制层ActivityController中拿到活动记录的id,基本写法如下:

 我们来到activity.html这个页面,找到删除按钮的位置,<td><button type="button" class="btn btn-danger btn-sm">delete</button></td>,这里如果想用thymeleaf提供的方式触发点击事件函数,可以这样写,th:onclick="doDeleteObject([[${aty.id}]])",即

<td><button type="button" class="btn btn-danger btn-sm" th:onclick="doDeleteObject([[${aty.id}]])">delete</button></td>

其中,想把通过thymeleaf的取值方式,${aty.id},得到所需活动记录的参数,做为js函数方法的入参传给函数,则thymeleaf中规定,必须在其外层包裹两个方括号(比如这里是[[${aty.id}]])才可以。这是thymeleaf中规定的格式:

<tbody>
    <tr th:each="aty:${list}">
        <td th:text="${aty.title}"></td>
        <td th:text="${aty.category}"></td>
        <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
        <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
        <td th:text="${aty.state==1?'有效':'无效'}"></td>
        <td><button type="button" class="btn btn-danger btn-sm" th:onclick="doDeleteObject([[${aty.id}]])">delete</button></td>
    </tr>
</tbody>

写完这个点击事件之后,我们后面需要定义doDeleteObject这样的一个函数,并为这个函数声明一个入参id,然后在函数体内,location.href="/activity/doDeleteObject?id="+id;,跳转到某一个地址去执行删除操作,当然,还可以给出一个提示,比如在点击删除按钮后,提示用户确定删除么,if(!confirm("确定删除么?")) return;,其中comfirm,它属于JavaScript中的一个JS函数,会弹出一个提示窗口,如果点击确定,confirm函数会返回一个true,非true就是false,return;就不会执行,因此会继续执行当前函数中的后续语句,如果点击取消,不删除,confirm就会返回一个false,非fasle就是true,则执行语句return;,结束执行当前的doDeleteObject方法:

<script type="text/javascript">
   function doDeleteObject(id){
       if(!confirm("确定删除么?"))return;
       location.href="/activity/doDeleteObject?id="+id;
   }
   //...
</script>    

执行效果如下:

 

点击确定后注意,我们没有真的删除这条记录,这时我们还没有做删除,看控制台执行结果,可以发现我们已经取到了这条记录ID:

 拿到这个id以后,我们就可以继续下一步操作,基于这个id进行删除活动记录的业务。另外,我们这里请求路径传参的方式是以问号对请求路径与请求参数进行分割的,这是典型的java路径传参方式,那这里还可以使用更为普及的restful风格的方式进行传参,即根据斜杠的方式传参,实现方式如下:

     @RequestMapping("/activity/doDeleteObject/{id}")
     public String doDeleteObject(@PathVariable Long id) {
         System.out.println("delete.id="+id);
         //...
         return "redirect:/activity/doFindActivitys";
     }

 相应的客户端,activity.html,请求路径url上就不是写问号了,直接是以斜杠连接:

function doDeleteObject(id){
 if(!confirm("确定删除么?"))return;
 location.href="/activity/doDeleteObject/"+id;
}

我们可以发现,依然是可以拿到请求传递过来的参数id的,这样我们就以传统的java里的问号传参方式和更为普及的restful的传参方式,分别拿到了请求参数id,但其实有的时候,这里的活动创建了,是不允许删除的,只不过我们为了便于对技术的理解而把删除操作 也做了一下,很多活动是到时间是自动删除的,当然这些需要根据具体的业务需求而定,比如一般要删除的话,首先应该检查活动的状态是什么,如果活动状态为有效,不允许进行删除,即得到活动id以后,首先判断这个活动是否正在进行中,如果是的话就不允许删除此活动,所以我们在删除活动之前,可能还需要在业务层做一个校验,校验活动当前的状态值,如果活动状态为0-禁止,则允许删除,否则活动状态为1-启动,则活动正在进行中,不允许删除,那这里,我们把删除的业务操作写一下,此时我们把id已经提交给后台,那基于id进行删除,那首先是后台的数据层,在ActivityDao接口里面,我们可能会定义一个方法 int deleteById(Integer id);,或者写这里写成 int deleteObject(Integer id);,去基于id执行一个活动的删除:

     @Delete("delete from tb_activity where id=#{id}")
     int deleteObject(Long id);

 那下一步,我们可能要去写业务层,业务层接口ActivityService中也是定义一样的方法: 

     int deleteObject(Long id);

 然后在业务层实现类ActivityServiceImpl.java中,实现这个deleteObject方法,那种真正的去执行delete删除之前,我们还要做些什么呢,那我们刚才说过了,在删除之前,我们还需要校验活动的状态,如果活动状态为有效,正在进行中,那就不允许删除,否则风险系数很高,你的活动没有结束呢,如果来了一个人进入后台一看,一不小心把活动删了,所以正在进行中的活动不允许进行删除,那这个校验呢,可以在客户端校验,如果在客户端校验的话,肯定是先在页面上拿到了要删除的那条活动记录的状态值,在点击delete操作的时候,就拿到了相应活动记录的状态值,如果状态值为有效,就不允许去提交这个删除请求,这是在客户端校验,那也可以在服务端校验,在服务端校验,就是我们在服务端业务层执行删除deleteObject操作时,在调用activityDao.deleteObject(id)方法之前,先查询了活动记录的状态值,假如状态有效就需要告诉客户端,现在不能更新,但如何告诉客户端这个活动状态有效,不能更新,即服务端该怎么返回这个信息到客户端,想要实现这个功能,最有效的方式是通过ajax去做的,不用ajax不是能做,太麻烦了,所以这里只需知道怎么去校验即可,暂不去实现校验功能:

    @Override
    public int deleteObject(Long id) {
        //校验活动状态?(正在进行中的活动不允许进行删除)
        //.........
        //删除活动
        int rows=activityDao.deleteObject(id);
        return rows;
    }

最后,我们在控制层ActivityController.java中,再调用一下业务层的这个实现类的deleteObject方法,控制层不做具体的业务,任何逻辑校验,它只是调用业务对象来执行业务,它做的是流程的这个控制,以及请求和响应数据的一个处理,即控制层就是拿客户端的数据,调用业务层方法,然后响应。

     @RequestMapping("/activity/doDeleteObject/{id}")
     public String doDeleteObject(@PathVariable Long id) {
         System.out.println("delete.id="+id);
         activityService.deleteObject(id);
         return "redirect:/activity/doFindActivitys";
     }

那重启服务,刷新,点击delete按钮,确定删除,发现数据就会真正的被删除了:

 我们发现如果这样一个一个删除,太麻烦了,那将来能不能在这做一个全选, 一个一个删除太麻烦了,当然是可以的;我们用模态框实现了创建活动的活动信息录入新增功能,很多场景都会有这种模态框的实现,虽然不是我们自己写一个模态框,但至少我们用了一下它,把它的内容改成自己的业务需求,以及删除和查询功能,我们都做了,将来随着活动数据越来越多,我们可能还要做分页,所以我们在此项目的基础上,可能还会写其他的一些业务,然后做一些业务方面的扩展,那我们如何基于我们的技术来实现我们的业务需求,那当我们还没有这种以业务为中心地去理解技术实现的思路时,那思路和代码哪个更为重要呢?比如,我们需要在创建活动时开启倒计时,在删除时需要对活动状态进行校验等等。那关于修改操作,最好是通过ajax去实现,因为修改这块数据的回显的写法,需要引入大量的js脚本,可能会有一定的难度,而保存和删除加一起也就5行代码左右,易于去分析和理解。

 工作能力:公司的teamleader一般会对员工有这样的一个定位,比如说,领导安排了一个任务,说这个业务你去把它实现一下,有人会说,我们没有学过啊,这个会让别人对你有看法,说明这个人不会去学习;也有人会说,您能告诉我是用什么技术么,我去学一下,说明这个人的主动学习能力会强,还有人可能会说,你给我一个晚上的时间,我明天给你一个结果,这就是非常有底气的,说明这个人不仅仅自学能力很强,还可以通过查询资料,找别人帮忙,能够解决这个问题,万一解决不了呢,那先答应下来,解决不了,明天再找理由呗!比说说我想了个钟各样的办法,查了什么什么资料,结果没有实现出来,我还需要一点点时间,一般teamleader还会给你一些时间。。。

那对于一些前端技术的实现,比如日期选择框,工作后可能10几年也不会写这个东西,而是用现成的工具,那这个现有的工具怎么做,比如bootstrap datepicker前端插件,那这个插件可以去网上下载它的静态资源,比如从github上下载的bootstrap-datetimepicker-master,这个就是datepicker官方里给出的内容:

 这个文档里有Bootstrap v2,还有Bootstrap v3的,那我们这里用的是Bootstrap v3的版本,所以我们一定是看的sample in bootstrap v3, 这个里面的内容,从这个目录里去找index.html:

 打开这个index.html,这个文件就是datepicker的demo案例,除了Bootstrap的css样式文件和js脚本外,首先发现在这个demo的head里引入了一个datetimepicker的css:

 其次,它又在这个body体内的最下面的js脚本部分,引入了datepicker的js脚本,还引入了一个使用什么语言的js脚本,它这个语言用的是bootstrap-datetimepicker.fr.js,这个fr是法语的吧!

 再往后,它又说你在初始化时,可以以以下3中形式进行初始化:

 不管是什么,我们去应用一个技术,重点于会用,而不是把它的所有的技术细节都搞明白了,所以我们现在就去试验,如何使用这个datepiker框架:

 那我们在这个activity的活动项目中就做了这样一件事,在项目的src/main/resources/static目录下添加了datepicker的这个静态资源文件datepicker,其中包括css和js两个子目录

 

 我们发现css和js目录下分别有两个名称相同的,但后缀一个是css,一个是min.css的两个文件,有min标识的文件,是压缩版的css,一般项目上线都是使用压缩版的文件,在压缩版的文件中去除了文件中的缩进结构,所有的代码可能就在几行之内,而未压缩版的,即min标识的文件是我们开发时用的,它的可读性就会好很多,保留了开发过程中原始的缩进与换行的标识。

首先我们引入boostrap-datepicker到我们的页面里:

<head>
... ...
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/datepicker/css/bootstrap-datetimepicker.min.css" rel="stylesheet" media="screen">
</head>
<body>
... ...
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="/jquery/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/datepicker/js/bootstrap-datetimepicker.min.js" charset="UTF-8"></script>
<script type="text/javascript" src="/datepicker/js/locales/bootstrap-datetimepicker.zh-CN.js" charset="UTF-8"></script>
... ... 
</body>

那对于datepicker如何使用,我们可以从网上或者去它的官方文档里去搜索,下面在我们的activity项目中使用如下:

<body>
...
    <div class="form-group" >
        <label for="startTimeId" class="col-sm-2 control-label">开始时间</label>
        <div class="col-sm-10">
            <input type="text" class="form-control form_datetime"  name="startTime" id="startTimeId"
                placeholder="start time">
        </div>
    </div>
    <div class="form-group">
        <label for="endTimeId" class="col-sm-2 control-label">结束时间</label>
        <div class="col-sm-10">
            <input type="text" class="form-control form_datetime"  name="endTime" id="endTimeId"
                placeholder="end time">
        </div>
    </div>
...
</body>
<script type="text/javascript">
    //datetimepicker函数由bootstrap-datetimepicker.min.js定义,用于初始化日期控件
    $('.form_datetime').datetimepicker({//这里的form_datetime为input标签中的class选择器
        language: 'zh-CN',
        format: "yyyy/mm/dd hh:ii",
        autoclose: true
    })
</script>

其中,form_datetime一般是写到input标签的class属性里面,然后通过类选择器得到这个input控件;再调用datetimepicker函数,此函数是由datepicker.js系统底层指定的,而这个函数的作用就是,在我们的页面加载完成以后,就初始化这个datetimepicker函数,language: 'zh-CN',为设置datepicker的语言环境为中文,format: "yyyy/mm/dd hh:ii",为设置日期的格式,hh:ii为小时和分钟,autoclose: true,表示选择日期以后自动关闭日期框窗口,这些规则都是由datepicker.js中定义,需要查询datepicker规范去设置,我们在使用时应该根据自己的业务需求去查询。效果图如下:

我们发现,这里还需要选具体的小时分钟,那如果不想选这个时间,这个datepicker怎么实现的,还是没有这个设置,暂时不得而知,就用这个带有小时分钟的形式,那如果这里在activity.html这里带了小时和分钟,那么我们在pojo类Activity.java中就需要为其中的对应的日期属性指定相应的格式,即在客户端指定什么日期格式,那么在服务器端的这个日期格式就需要和客户端的日期格式,必须是匹配的,否则可能会出现400异常:

 

对于日期框,也可以使用js的默认日期框(这样就可以解决datepicker还必须选择小时和分钟的情况),直接把input的type属性改为date即可,而无需下载其他样式文件,但这种方式存在很多兼容性问题(经过试验,不同的浏览器对date的支持并不友好,所以此处应用了datepicker日期插件):

另外,对于日期框,浏览器还可能出现自动记忆填充功能,解决方案如下:

 那接下来,我们点击创建活动按钮,创建一个活动保存到数据库,title:企业活动,类型:交易培训,开始时间:2020/08/07 10:20,结束时间:2020/08/07 11:20,备注:dafjdajf,看看是否能保存成功:

 我们至少先写出来,如果说后面还有其他的一些需求,可以再去改,那么点击save保存后,我们发现保存成功了:

 但是,我们发现在日期的位置,没有显示小时和分钟,只有年月日,那我们还想显示小时和分钟,那说明我们日期的格式化在我们客户端activity.html这里,还有点小问题,如果我们后面还想显示小时和分钟,就在客户端获取日期时,将其格式为'yyyy/MM/dd HH:mm'即可:

<tbody>
    <tr th:each="aty:${list}">
        <td th:text="${aty.title}"></td>
        <td th:text="${aty.category}"></td>
        <td th:text="${#dates.format(aty.startTime, 'yyyy/MM/dd HH:mm')}"></td>
        <td th:text="${#dates.format(aty.endTime, 'yyyy/MM/dd HH:mm')}"></td>
        <td th:text="${aty.state==1?'有效':'无效'}"></td>
        <td><button type="button" class="btn btn-danger btn-sm" th:onclick="doDeleteObject([[${aty.id}]])">delete</button></td>
    </tr>
</tbody>

实现效果如下:

到这里Activity活动模块的基本任务就算完成了,那基于这个小案例,我们继续扩展一些新的内容,什么是健康检查,什么是热部署,还有Lombok插件的应用等,首先我们先看一下健康检查的应用,那什么为健康检查,就是在SpringBoot当中,它还给我们提供了一个监控功能,比方说监控你的url和bean之间的这种映射,监控你的bean对象是否注入到我们的容器中,监控你的系统当中的一些环境的配置都可以,其实这个健康检查无非就是一个监控功能。那要在STS中去应用它,首先我们需要在项目中的pom.xml配置文件中添加健康检查的依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

添加依赖以后,我们启动服务,然后在浏览器中输入如下地址:http://localhost/actuator/health (默认80端口可以省略端口号),如显示UP,表示当前项目的状态是ok的。

 假如希望查看更多的actuator的选项及信息,想监控更多的信息,可以在Springboot中的配置文件application.properties里添加如下语句(生成环境不加):management.endpoints.web.exposure.include=*,假如我们应用的是阿里云构建的SpringBoot项目,那在创建项目时,它的application.properties文件中默认这句话就有,假如是spring.io.starter构建的SpringBoot项目,那就需要手动去添加上这句话:

更改了配置我们需要重启服务,然后我们在浏览器地址栏再输入一个地址:http://localhost/actuator/beans 查看所有的spring 容器中的 bean 信息:

 可以看到输出了一堆的beans,但这个看起来很不舒服,我们可以通过Ctrl+F,去找自己写的bean,比如activityService,默认情况下的输出结构确实不够爽,那想让这个结构呈现的时候更加清楚,可以对谷歌浏览器安装一个jsonView插件,就可以以更加结构化的方式去查看bean的信息了,但要安装这个插件,首先能够上Google网站才能安装,或者是在本地有下载离线版的,当然如果在线安装,则必须保证网络是畅通的,比如说打开Google设置中的更多工具,有一个扩展程序,

 在打开的窗口中点击左上角的菜单图标,就可以找到打开 Chrome 网上应用店:

 

 打开Chrome网上应用店,搜索JsonView,可能会出现多个JsonView,哪一个评价好,就用哪一个,我们就选第一个2832人的就可以了,添加至Chrome:

 安装成功以后,我们再次访问http://localhost/actuator/beans ,就可以发现装了JSONView插件以后,它会把文本以结构化的方式去显示,然后搜索activityService,就可以找到我们自己的Service,其内容包括类型type: "com.cy.pj.activity.service.impl.ActivityServiceImpl",作用域scope: "singleton",没有给它起别名aliases为空,具体这个类所在的位置resource: "file [...]",它依赖于是谁dependencies["activityDao"];这个Service是依赖于activityDAO的,这里都可以去查看的。

 利用浏览器去查看健康检查的监控信息了解即可,我们重点要掌握的是健康检查,还可以直接在sts工具的Boot Dashboard(Boot面板)中选中项目,查看其属性(show properties):

首先我们必须要确保项目已经启动了tomcat,然后打开Boot DashBoard面板,选中启动项目,然后点击右上侧工具栏的一个小图标Show Properties:

 打开ShowProperties视图,首先看Request Mappings是映射,这映射里面就记录了我们写的添加:/activity/doSaveActivity,删除:/activity/doDeleteObject/{id},和查询:/activity/doFindActivitys,这里呈现了url对应的是哪一个方法,这就叫做监控;

 然后,还有一些Beans,在Beans这个环境里面,我们可以找到我们自己写的bean,有activityController,activityDao和activityServiceImpl,其他的那些就不是我们自己写的,系统底层自动生成的。

打开activityController,我们可以发现它所在的是哪个类,它所依赖的是activityServiceImpl这个对象,以及这个activityController需要注入给谁么,它不需要注入给谁;

然后这个activityServiceImpl呢,它依赖于activityDao,activityServiceImpl会注入给谁,注入给activityController,这个结构是非常清楚的:

 最后,看这个activityDao依赖于谁,activityDao依赖于一个是工厂sqlSessionFactory,一个是sqlSessionTemplate,依赖于这两个对象,那activityDao,它会注入给谁呢,注入给我们的activityServiceImpl:

那url和Beans的信息都看完了,最后还有一个就是环境Env,比如这个applicationConfig: [classpath:/application.properties],这是我们自己的那个配置文件,我们自己配置的那个环境,会在这个位置显示:

比如说,我们可以看到我们配置的,

健康检查:management.endpoints.web.exposure.include=*

映射文件的路径:mybatis.mapper-locations=classpath:/mapper/*/*.xml

连接池的配置:

spring.datasource.username=root

spring.datasource.url=jdbc:mysql:///dbactivity?serverTimezone=Asia/Shanghai&characterEncoding=utf8
spring.datasource.password= ******

前端框架thymeleaf的配置:

spring.thymeleaf.prefix=classpath:/templates/modules/

spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html

日志的配置:logging.level.com.cy=debug

以上这是我们自己配置的内容,除了我们自己配置的内容,还可以看到这块还有一些系统的环境systemEnvironment:

 比方说,默认情况下,我用的那个jdk在哪个位置:JAVA_HOME = C:\CGBSoft\First\jdk1.8.0_45,这里都属于一些监控信息。那这个Show Properties窗口,在打开的时候可能没有显示,是有这种可能的,有的时候这个窗口它会卡顿,它反应慢,这时可以把这个窗口来回拖动一下,基本上都没问题:

 那健康检查其实就是SpringBoot提供给我们的一个监控,那通过这个监控,我们可以在监控里面得到什么样的信息,解决什么样的问题呢?首先在Request Mappings里,一看就是url映射,那么通过这里,可以去检测什么问题呢,比方说可以通过此映射检测404问题,404问题无非就是你访问的url,在我们这个RequestMappings里面找不到对应的资源么,那就可以在这里看看你的url对应的资源是否存在;然后在Beans里面也有一部分信息,Beans这部分内容里面可以去解决这个问题,通过这里可以对Spring中的bean进行检查,例如可以发现NoSuchBeanDefinitionException,当在这个Beans里面都找不到bean,那启动时一定是没有的,就是当我们出现了NoSuchBean这样的异常,但又不知道是哪个bean,那首先要找到自己的那个类,在Beans这里就可以检查一下;而Env这部分里面定义的是什么呢,就是我们说的那个配置信息,这部分配置信息包含我们自己定义的,也包含系统帮我们去定义的一些配置信息:

我们点开这个Env,可以看到如下信息:

 我们再点开applicationConfig,这个应用程序配置都是我们自己的配置:

 比方说:我们配置的端口server.ports:

 

 再比方这个commandLineArgs这个参数:

还有一些什么系统属性systemProperties,有很多,比如说我们现在用的java指定的版本,java.specification.version=1.8;比如我cpu的一些配置信息,sun.cpu.isalist=amd64;还有我们的编码方式sun.jnu.encoding = GBK;等等等等,这里都可以去看到:

 以及我们自己配置的那部分内容:

这个就叫做监控,也就是说所谓的,对url,对bean,以及系统环境的一个监控,在这里面我们都能看到。 

那下一个话题是热部署,对于热部署来讲,其实在我们的项目当中,假如我们改了java代码,我们还需要手动去启动我们的tomcat服务器,才能够去加载我们的这个改动,那么假如我们希望,当我们改了代码以后自动去加载这些配置信息,那就需要在项目的pom.xml配置文件中添加一个依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>

这个依赖叫做自动部署依赖,如果我们没有配置热部署,没有引入热部署的包依赖,无论修改了项目中的任何代码,控制台都没有反应:

我们的tomcat服务器是不会自动重启的,我们必须手动重新部署项目。而一旦我们配置了热部署:

当修改一些文件时,tomcat服务器都会自动重启,但是对于硬件配置比较低的电脑,不建议配置热部署,而是手动去重启,因为我们每写一部分,它都要重启一下tomcat,其实是非常占用内存的,因为重启的过程,它需要涉及到资源的销毁,资源的重建,如果内存配置比较低,那就不要去开启热部署了。那加载了热部署,一旦我们修改了如下代码,tomcat就会自动重启:

(1) src/main/java目录下的源代码;

(2) src/main/resources目录下的application.properties配置文件;

(3) src/main/resources目录下的MyBatis的Mapper映射文件;

(4) 项目目录下的包依赖的pom.xml配置文件;

比方说,我们在项目启动类CgbActivity01Application.java的位置,加一个换行或者加一个输出语句; 

 比如在application.properties配置文件中配置一个server.servlet.context-path,或者配置tomcat能支持的最多线程数:server.tomcat.threads.max=256 (就类似于任务调度时底层Tomcat线程还会去启动另外一个线程Timer,启动那个线程和Tomcat线程是不一样的,Tomcat线程主要是负责处理客户端的请求);

比方说,我们在映射文件ActivityMapper.xml注释掉或解注释掉一段sql语句;

比方说,在项目的pom.xml文件中添加或去掉一个依赖,比如这个健康检查依赖:

以上资源的改动都需触发热部署,但是如果改动的是静态资源,比如src/main/resources/templates下的html页面,以及改动src/test/java目录下的测试类的时候,都不会重启,所以说,了解了项目结构以后,所谓的热部署就是我们修改了某些资源以后,tomcat会重启,来加载我们修改后的资源,而哪些资源修改后会触发热部署机制,自动重启tomcat加载资源,哪些资源的改动并不会进行热部署:

 在目录结构中,有一个目录是src,这个src的内容是不需要我们去写的,我们在src/main/或者src/test等目录下面写的所有内容,都是存储在src的目录下;然后src目录下面生成的.class文件,还有我们的配置文件,都会存储到target目录下面,但是我们是无法点开这两个目录去查看的,只有进入工作区才可以看到,再下面的HELP.md,mvnw和mvnw.cmd,这是使用maven工具创建项目时自带的一些文件,即这个就是我们的model。

 说明:当我们修改了src/main/java目录下的java文件或者修改了src/main/resources目录下的配置文件时,默认都会重启你的web服务器,但是修改了测试类或html文件不会自动重启和部署,但是假如希望修改了html,在重启tomcat的情况下也能看到页面模板内容的变化,需要配置spring.thymeleaf.cache=false,这样即便不重启服务器,只需在浏览器端刷新下页面也是可以进行实时的页面更新加载的。

那热部署到这里就告一段落了,下一个内容是Lombok插件的应用:那为什么要使用Lombok呢?首先在我们项目当中的会创建很多很多的pojo类,在这样的类里面,我们写好了属性以后,我们会通过sources,自动生成一些set/get等方法,假如我们写好了这个pojo类里面的属性以后,想让这些set/get/toString等方法由系统帮我们添加,而无需手动去添加,就可以通过Lombok帮我们实现,这样的话,至少我们源代码的代码量会减少,即.java代码不用我们自己去写,但编译后生成的.class文件里面会有,或者说在.java编译成.class的时候,我们希望在这个class里面植入一部分代码,那么就可以使用Lombok。

那么Lombok是什么,它就是一个第三方库,即由别人写好的,提供出来的一组API,我们可以基于Lombok中提供的API,在我们的.java程序编译成.class文件的时候,自动给它织入一些方法,我们的点java程序中没有这些方法,但是把.java程序编译成.class文件的时候,Lombok会自动添加这些方法;

那么如何使用Lombok呢,首先添加依赖:

<dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
</dependency>

另外,SpringBoot其实默认集成了这个插件,那有的时候,如果不想这么去拷贝依赖,那也可以在我们的工程当中,选中pom.xml,右键选择Spring,选择Edit Starters,如果联网还可以的话,就可以在弹出的Edit Spring Boot Starters这里搜索,找到Lombok插件:

 

如果之前用过Lombok,就会显示Lombok默认已经选中了:

 

 那我们已经在项目当中,把Lombok的依赖添加上了,可以在Maven Dependencies目录下找到这个依赖:

 可以在MavenDependencies下找到Lombok,复制它的Copy Qualified Name,然后在windows窗口直接输入此地址,直接回车:

发现回车不可以,说明了我这个jdk,它不是安装版的,它是配置了一个绿色版的,在这里面拷贝过来了,如果要是一个直接安装版的jdk,就可以直接把Copy Qualified Name,这个复制的.jar文件的地址粘贴到windows窗口上,一回车就会去自动运行它。

 

 那如果不是安装版的jdk,可以去掉Copy Qualified Name后的文件名,进入到lombok.jar文件所在的目录中:

我们可以选中这个lombok的jar文件,鼠标右键打开命令行,如果这样找不到的命令行,还可以在这个目录的windows窗口上直接输入cmd,或者通过win+R+cmd进入命令行,然后输入目录地址切换到此目录下,通过java -jar lombok-1.18.12.jar命令,回车即可(说明:有的可能双击.jar文件就可以执行,有的可能双击不了,因为jdk是解压版的,不是直接安装的。 ):

然后,可能会弹出一个警告框Can't find IDE,不用去管它,直接点击确定即可:

然后,在Specify location..这个位置选择你的sdk要安装在什么位置:

这里要确保现在的Lombok装在正在使用的那一个STS的工具上面(已经添加有lombok依赖),然后点击Select:

 然后点击install:

最后显示安装成功,叉掉这个窗口即可:

 这里需要注意的是,STS所在的路径中不允许中文或特殊符号,比如$&等,否则我们的STS可能就启不来了,还得需要做额外的一些配置,假如按照成功以后,在我们的STS所在的目录下就会多出一个lombok.jar文件,有了这个文件以后,那接下来还得去修改一下STS所在目录下的SpringToolSuite4.ini文件:

 在这个SpringToolSuite4.ini文件里面,它会对lombok做一个自动的配置,打开这个文件,它会多出这么一行:javaagent:D:\CGBIIICLASS\TOOLS\sts-4.7.0.RELEASE\lombok.jar:

 即安装好lombok以后,就会在此文件里多出这么一行记录,那么多出这么一行记录,它指定了你安装的这个lombok.jar,这个文件所在的目录,即我们这个STS工具的目录下多出的lombok.jar所在的目录,那么它就会在这个ini配置文件有这样的一个记录,假如这个指定的目录存在中文或特殊符号,比如&符号,它最终在编译的时候,有可能会在&符号这里替换一个斜杠,那么lombok就找不到了,找不到以后,那这个位置可能就要报错了,STS工具启动都起不来了:

所有步骤安装完成以后,我们重启STS工具,看看STS是否能成功启动,如果可以正常启动,说明安装到这里是没有问题的,那启动成功以后,下一步就可以去验证我们的lombok是否安装成功或者说lombok有效了,我们可以在CGB-ACTIVITY-01这个项目里面,把这个pojo类Activity.java中的所有的set/get方法,以及toString方法全部去掉,只留有属性信息,这些属性会提示有警告标识,然后我们在Activity类之上去追加一个注解@Data,当我们加上这个@Data注解以后,就会发现这些属性上的警告没有了,这时就说明我们的lombok已经安装成功,而且可以正常使用,即系统当中会帮我们把这个类的属性,自动生成相应的get/set等方法,那么在我们这个Activity.java这个类上,就正确的使用了lombok里面的特性,在这个pojo类编译成.class文件的时候,lombok自动会帮我们添加set/get以及toString方法。

 Lombok安装成功的标志,第一个,在我们的开发工具sts当中会有一个lombok.jar文件,第二个,在我们的开发工具sts根目录下的SpringToolSuite4.ini这个配置文件里面,会在最后多出一行信息,指明了lombok.jar文件所在的路径,这里即sts的根路径,需要注意的是这个lombok.jar文件的所在的目录如果有中文字符,那么开发工具sts可能就启动不了了,此时可以将这个lombok.jar文件剪切到一个英文的目录下面,然后去修改一下这个SpringToolSuite4.ini文件中相应配置信息的路径位置。
安装好以后,首先我们可以重启安装了lombok的IDE,然后应用lombok注解(例如@Data注解),BUG分析:1.那当我们使用lombok注解应用无效时,首先确定正在使用的STS安装的lombok,如果还不行的话,强制maven update更新项目。还有一些其他常见的问题(FAQ):第一个,STS安装了lombok以后,创建新的项目是否还需要再次安装lombok?答:不需要,但需要添加lombok依赖,但如果要是把项目拷贝到另一个STS开发工具时,那么这个STS必须也要安装lombok,否则就无法应用lombok注解;第二个,lombok安装了以后,如果对lombok进行卸载,这个卸载就两个步骤,第一步删除lombok.jar文件删除(如果是开发工具使用的是idea也是同样如此),然后第二步把sts根目录下的SpringToolSuite4.ini文件里指定lombok.jar文件的那一行信息也删掉即可;

那lombok安装成功以后, 我们要想应用lombok,首先把安装了lombok的开发工具重新启动一下,然后在指定的类上面,比如Activity.java这个类来讲,在其上添加@Data注解,当我们这个类在编译时(将.java文件编译成.class文件),lombok会自动基于类中的属性添加相关方法到class文件,我们应该知道,对于我们sts这个软件来讲,在我们Ctrl+s一保存,我们写的.java就会编译成.class文件,那我们现在安装了lombok以后,在我们的sts开发工具的SpringToolSuite4.ini配置文件中会多出javaagent这么一行信息,这个javaagent的意思就是当你现在这个STS当中,Ctrl+s去保存这个被lombok注解修饰的类的时候,比如这个Activity.java,系统底层会检测这个java程序上面,也就是.java类上面有没有lombok的注解,如果有的话,那么lombok,也就是lombok.jar这个文件就会启动它内部的API,为我们这个.class文件添加set和get方法,还有toString方法都会有,那实际就是说目前,我们使用lombok,它把原有STS对我们.java程序进行编译的那个过程做了一定的拦截,

拦截到以后,有lombok.jar来进行我们的.java程序的编译,把编译好的一些内容,比如Activity.java这个类,编译好了以后,还会向这个类里面再添加一些set和get方法,那我们已经在Activity .java这个类里面添加了@Data注解,那lombok是否真的已经为我们添加了set和get方法,我们可以测试一下,比如我们在src/test/java/com/cy这个测试包下面,专门在这写一个测试类LombokTests,看一下有没有这个lombok,首先new一个Activity aty,然后aty.setId(100L);,aty.setTitle("互动答疑0");,发现是可以调用set方法的,那么这个set方法,我们并没有去写,但是生成了,我们在输出一下aty,System.out.println(aty);,执行此测试方法,单元测试成功:

这里我们输出了对象的内容,而不是对象的那种地址的表现形式,那么说明我们这里面除了添加了set和get方法,那么在输出对象的时候,也调用了对象的toString方法,而我们也没有重新toString,说明在我们Activity这个类的上面加了@Data以后,它帮我这个类生成了set/get,还有toString方法,还有hashcode,还有equals方法,它其实都帮我们生成了,都是由@Data注解描述此类时,但是如果只想要set和get方法,不想要其他的方法,那就可以直接加@Setter和@Getter,去掉@Data,这时就没有所谓的toString方法了,这时再对这个对象输出时,就只是我们的类全名和对象的hashcode值,比如com.cy.pj.activity.pojo.Activity@28a9494b,那如果我们再在这个类上加上@AllArgsConstructor,此时会发现这个Activity这个类就会报错,因为@AllArgsConstructor表示会在这个类里面添加一个全参的构造函数,我们在这个类当中添加了带参的构造函数,那么无参构造函数的默认就没有了,所以这是在LombokTests这个测试类中的测试方法testLombok,在new Activity()无参构造器的时候就会报错:

 那我们在Activity.java这个pojo类中再添加一个无参的构造器,添加注解@NoArgsConstructor,如果还想添加toString方法,就添加注解@ToString:

 这里的这些注解仅仅起到一个标识性作用,真正在这个类里面添加方法,不是说注解去添加,注解起到了这个标识以后,是底层会赋予这个类一些额外的特性,所以这里面要对这个注解要有一定的认识,这些这些可以被称为元数据,即描述数据的数据都叫做元数据,这里提到的都是最常用的,当然还有其他的注解,比如说,我们在我们的测试类LombokTests上,又加了一个注解@Slf4j,然后在testLombok方法内的最后,再做一个输出,log.info("title is {}",aty.getTitle());,可以发现我们在这个类里可以直接使用log,但是我们在这个类里面并没有定义log,却依然可以使用它,只是因为我们加了一个@Slf4j注解,我们这个log就会自动生成;这个Slf4j之前有写过,是这样的,private static final Logger log = LoggerFactory.getLogger(LombokTests.class);,都是利用这个Slf4j这个包下面的,这里还使用了一个设计模式,叫门面模式(阿里手册有说明)。假如在LombokTests类中加上这样一句话,那么lombok的@Slf4j这个注解就不起作用了,有一个警告:

 为什么@Slf4j上有一个警告,是因为当我们自己去创建private static final Logger log = LoggerFactory.getLogger(LombokTests.class);,这样一个log对象以后,lombok就不再为我们创建了,那这个@Slf4j就写也行,不写也是可以的,其实就不用写了;那我们加了@Slf4j这个注解以后,那系统底层会基于这个注解的描述,为这个LombokTests这个类里面自动生成这样的一行,private static final Logger log = LoggerFactory.getLogger(LombokTests.class);,那这句话就可以注释掉了,这个log对象照样可以使用,即当我们的类上使用了@Slf4j注解时系统底层会自动为我们的类添加一行声明log对象的代码,但是这行代码中参数中传入的类加载器的类名,是@Slf4j这个注解描述哪个类,这个参数就是哪一个类的点class文件,我们运行看一下它的输出:

 单元测试成功,上面是toString方法,下面就是INFO就是我们自己输出的com.cy.LombokTests :title is  互动d答疑,Lombok具体的日志信息,这就是Lombok的应用,但是当我们引入了@Slf4j注解以后,发现这个log没有生成,说明这个STS这个软件有可能Lombok没有安装成功。

 

那么Lombok解决的问题就是,当我们写了一个.java程序,我在这个.java程序里面可能没有写LoggerFactory.getLogger(LombokTests.class);这样的语句,lombok可以帮我们自动生成这样的一条语句,也就是说我们的程序会变的,好像越来越智能了,有些代码不用我们写,也能够生成,这都是底层帮我们做的,比如说Lombok帮我们做的。Lombok仅仅起到的作用是让我们自己写程序的时候简单一点,其实它的.class文件一点都不简单,它的.class文件生成的还是和我们原先写的没有Lombok简化之前的代码的时候一样的内容,只是lombok帮我们在写java代码的时候

 

附录A:基本模板

使用以下给出的这份超级简单的 HTML 模版,或者修改这些实例。我们强烈建议你对这些实例按照自己的需求进行修改,而不要简单的复制、粘贴。

拷贝并粘贴下面给出的 HTML 代码,这就是一个最简单的 Bootstrap 页面了。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
      <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <h1>你好,世界!</h1>

    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </body>
</html>

JavaScript 在stackover.com 程序员的交流营中,最受欢迎的编程语言之一:JavaScript是客户端最强悍的一种脚本语言,因为它的生态做的特别好,不仅仅在浏览器端,手机端,都可以去使用。

附录B:基本模板

一、server.servlet.context-path配置的作用

定义: server.servlet.context-path= # Context path of the application. 应用的上下文路径,也可以称为项目路径,是构成url地址的一部分。

server.servlet.context-path不配置时,默认为 / ,如:localhost:8080/xxxxxx

当server.servlet.context-path有配置时,比如 /demo,此时的访问方式为localhost:8080/demo/xxxxxx

二、springboot 2.0变革后的配置区别

1、springboot 2.0之前,配置为 server.context-path

2、springboot 2.0之后,配置为 server.servlet.context-path

三、一个思考

原来的运营项目(已上线),配置文件添加 server.servlet.context-path 配置后,需要在thymleaf 中进行action请求的追加吗?

答案:不需要。

栗子:

前端页面采取form请求

<form th:action="@{/user/userLogin}" method="post" id="userLogin"></form>

action拦截接受方式

 
@Controller
 
@RequestMapping("/user")
 
public class LoginController {
 
 
 
@PostMapping("/userLogin")
 
public String userLogin(HttpServletRequest request, Model model) {
 
 

原项目的基础上,追加一个配置

 
server:
 
port: 8080
 
servlet:
 
context-path: /demo

只需要再开始进入首页时,追加 localhost:8080/demo ,后续的thymleaf中的href和action等无需添加/demo 。

参考文献:https://blog.csdn.net/qq_38322527/article/details/101691785

键key上为什么有两个冒号

posted @ 2020-08-06 16:43  HarryVan  阅读(798)  评论(0编辑  收藏  举报