Content Table

使用 REST

Spring MVC 提供了 REST 风格的注解支持,使用 GetMapping, PostMapping, PutMapping, DeleteMapping。JS 的 AJAX 原生支持 GET, PUT, POST, DELETE 请求,但是 Form 表单只支持 POST,不支持 PUT 和 DELETE,为了让 Form 表单也能够使用 REST 的风格进行提交,需要给表单额外提供一个参数 _method:

  • _method 为 put 表示 PUT 请求
  • _method 为 delete 表示 DELETE 请求

服务器端还需要一个 Filter 把 Form 表单的 REST 请求转换为 Spring MVC 识别的 REST 请求。

Freemarker 语法

FreeMarker 是一个用 Java 语言编写的模板引擎,它基于模板来生成文本输出。FreeMarker 与 Web 容器无关,即在 Web 运行时,它并不知道 Servlet 或 HTTP。它不仅可以用作表现层的实现技术,而且还可以用于生成 XML,JSP 或 Java 等。

处理 Ajax 请求

Spring MVC 的 Controller 处理 AJAX 请求很简单,只需要在方法的前面加上 @ResponseBody 即可。
Controller 的方法一般返回 String(可以是JSON, XML, 普通的 Text),也可以是对象。

返回 Json 字符串

  1. Controller 中添加方法

    1
    2
    3
    4
    5
    @GetMapping("/ajax")
    @ResponseBody // 处理 AJAX 请求,返回响应的内容,而不是 View Name
    public String ajaxString() {
    return "{\"username\": \"Josh\", \"password\": \"Passw0rd\"}";
    }
  2. 访问 http://localhost:8080/ajax

    输出: {username: "Josh", password: "Passw0rd"}

Freemarker 集成

JSP 和 Freemarker 都用于显示层,但是都有自己的优缺点。

Freemarker 的优点:
  1. 不能编写 Java 代码,可以实现严格的 MVC 分离,可维护性好
  2. 性能不错,比 JSP 快
  3. 对 JSP 标签支持良好
  4. 内置大量常用函数
  5. 宏定义非常简单(类似 JSP 标签)
  6. 使用表达式语言
  7. 美工和技术的工作分离(例如命名为 .htm 的格式,不需要经过 Server 就能在浏览器里看到效果,JSP 这一点不太方便)
Freemarker 的缺点:
  1. 不是官方标准
  2. 用户群体和第三方标签库没有 JSP 多

项目框架

工程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
├── build.gradle
└── src
├── main
│   ├── java
│   │   └── com
│   │   └── xtuer
│   │   ├── bean
│   │   ├── controller
│   │   │   └── HelloController.java
│   │   └── service
│   ├── resources
│   │   └── config
│   │   ├── application.properties
│   │   └── springmvc-servlet.xml
│   └── webapp
│   └── WEB-INF
│   ├── page
│   │   └── hello.html
│   ├── static
│   │   ├── css
│   │   ├── img
│   │   ├── js
│   │   └── lib
│   └── web.xml
└── test
├── java
└── resources

开发简介

本系列文章的目的是使用当前比较流行的技术,由浅入深、从入门开始进行介绍,最终开发一个具有完整功能的网站。

相关技术

  • 后端框架: Spring MVC
  • 页面模版: Thymeleaf
  • 数据库: MySQL
  • 持久层: MyBatis
  • 日志框架: Logback
  • 架构风格: RESTful
  • 访问安全: Spring-Security(登录管理,权限管理)
  • 项目管理: Gradle(自带热更新功能)
  • 前端框架: Vue、iView

Tomcat 部署

部署工程为 Tomcat 的默认工程,工程的 war 包为 web-mix.jar

  1. 创建目录 /Users/Biao/Desktop/data

  2. 复制 web-mix.jar 到目录 /Users/Biao/Desktop/data

  3. 解压 web-mix.jar 得到目录结构 (Tomcat 启动后不会自动解压)

    1
    2
    3
    4
    5
    6
    7
    data
    ├── web-mix
    │   ├── META-INF
    │   │   └── MANIFEST.MF
    │   └── WEB-INF
    │   ├── asset
    │   ├── ......

    解压命令: rm -rf web-mix; unzip web-mix.war -d web-mix

  4. <tomcat>/conf/Catalina/localhost 下创建文件 ROOT.xml:

    1
    2
    3
    <Context path="/" docBase="/Users/Biao/Desktop/data/web-mix"
    debug="0" privileged="true" reloadable="false">
    </Context>

    docBase: 工程所在路径
    path: 工程的 context path

  5. 启动 Tomcat

  6. 访问 http://localhost:8080 显示的是上面工程的页面,而不是 Tomcat 默认的主页

    不用删除 <tomcat>/webapps/ROOT,继续放在那里就可以了

使用 Lombok 自动生成 Getter and Setter

根据 Java Bean 的规范,Bean 就是一个简单的类,主要是属性和访问函数 Getter and Setter 等,都是模版性的代码,虽然有 IDE 帮助我们自动生成,但是代码打开后全是一大堆的访问函数,看上去也不舒服,更郁闷的是,有时候 Bean 有几十个属性,添加一个新的属性后也很容易忘了添加相应的访问函数,而 Java 的很多框架对属性的访问都是使用反射和访问函数来查找的,由于缺少访问函数导致有时候后端得到了数据,但是前端始终缺几个数据,逻辑看上去又没问题,很难得一下发现问题在哪里 (已经发生过好几次)。现在好了,我们可以使用 Lombok 来自动的为 Bean 生成访问函数。

一般使用下面 3 个注解就足够了,没必要搞得更麻烦:

  • @Getter
  • @Setter
  • @Accessors(chain=true)

虽然 @Data 的功能很强大,但是阅读上不够直观,所以不推荐使用。

@Data is equivalent to @Getter, @Setter, @RequiredArgsConstructor, @ToString and @EqualsAndHashCode.

Lombok 不会影响程序的运行性能,它使用 javac 的插件机制在编译阶段生成访问函数到 class 的字节码里,和我们直接写没有什么区别,反编译一下生成的 class 文件就一目了然了。

jQuery 的 REST 插件

使用 REST 风格提交请求时,Content-Type 规范的来说应该用 application/json,但是服务器端获取请求的参数时必须从 Request Body 中获取,有些框架对从 Request Body 中获取数据支持不好,虽然 SpringMVC 中能够使用注解 @RequestBody 从 Request Body 中获取数据,但这时不能使用 Filter 进行 XSS 过滤,总是感觉不太方便,推荐尽可能的使用 Content-Type 为 application/x-www-form-urlencoded,原因下面进行解释。

这里使用 SpringMVC 作为后端处理请求进行介绍,SpringMVC 提供了一个 Filter HiddenHttpMethodFilter,把 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求,参数中 _method 值为 PUT 的请求分发为 PUT 请求,为 DELETE 请求分发为 DELETE 请求,实现了普通表单的 REST 风格提交,这样同时可以使用 @RequestParam 获取参数的值:

  • Content-Type 为 application/x-www-form-urlencoded + HiddenHttpMethodFilter
    • 优点: 服务器端处理 GET, PUT, POST, DELETE 时可直接把参数映射为对象,或则都使用 @RequestParam 获取参数,使用形式一致、简洁
    • 缺点:
      • 不符合标准的 REST 规范
      • 参数是按照 key/value 的形式发送的,和普通表单的参数形式一样,有兴趣的可以在 Chrome 的 Network 中查看请求的 Headers
      • 不方便传递复杂对象,例如 value 又是一个 Json 对象,不过估计 90% 的情况简单的 key/value 就够了
      • PUT 时参数中需要带上 _method=PUT,DELETE 时参数中需要带上 _method=DELETE
  • Content-Type 为 application/json
    • 优点: 符合标准的 REST 规范,GET 处理和上面的一样,但是 POST, PUT, DELETE 的参数是序列化后的 JSON 字符串,能够传递复杂的对象
    • 缺点:
      • 服务器端直接参数映射为对象,或则 GET 时使用 @RequestParam 获取参数,POST, PUT, DELETE 使用 @RequestBody 获取参数到 Map 中,然后再从 Map 中获取一个一个的参数,非常繁琐
      • GET 和 POST, PUT, DELETE 获取参数的形式不统一,一个用 @RequestParam,其他的用 @RequestBody,需要脑子转换一下
      • 浏览器端 PUT, POST, DELETE 请求传递的 JSON 对象需要序列化后才能传给服务器端

总结下来,在 SpringMVC 中推荐使用 application/x-www-form-urlencoded + HiddenHttpMethodFilter 的方式实现 REST 的请求,就是为了获取参数时形式统一,当需要传递复杂的参数时,例如属性是多层嵌套的对象,Json 对象的数组,这时再使用 application/json 的方式。

为了简化 Rest Ajax 的访问,对 jQuery 的 Ajax 进行了简单的封装成插件 jQuery.rest,下面的例子展示了更新用户名的原始实现和使用 jQuery.rest 简化后的代码:

1
2
3
4
5
6
7
8
9
10
$.ajax({
url : '/users/1/username',
data : JSON.stringify({name: 'Bob'}),
method : 'PUT',
dataType : 'json',
contentType: 'application/json'
})
.done(function(result) {
console.log(result);
});

如果每个 REST 的请求都像上面这样写一遍:

  • GET 时 data 不能序列化
  • PUT, POST, DELETE 时 data 需要序列化: JSON.stringify(data)
  • 请求不同时 method 也不同
  • dataType 和 contentType 是固定的

这么多参数,很容易出错。使用下面实现的 rest 插件后,简化如下,只需要关心参数和回调,不需要处理其他额外信息,而且 $.rest.update 名字也更语义化,一看就知道是更新操作:

1
2
3
4
5
6
7
$.rest.update({
url : '/users/1/username',
data : {name: 'Bob'},
success: function(result) {
console.log(result);
}
});