在我们前⾯的代码例子中,都已经设置了响应数据, Http 响应结果可以是数据, 也可以是静态页面,也可以针对响应设置状态码, Header 信息等.
创建前端页面 index.html(注意路径)

DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index⻚⾯title>
head>
<body>
Hello,Spring MVC,我是Index⻚⾯.
body>
html>
@RestController
public class IndexController {
@RequestMapping("/index")
public Object index(){
//返回index.html
return "/index.html";
}
}
运行结果:http://127.0.0.1:8080/index

结果却发现, 页面未正确返回, http响应把 “/index.html” 当做了http响应正文的数据,那 Spring MVC 如何才能识别出来 index.html 是⼀个静态页面, 并进行返回呢?
我们需要把 @RestController 改为 @Controller
正确代码如下:
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
}
再次运行: http://127.0.0.1:8080/index

发现页面正确展示了
@RestController 和 @Controller 有着什么样的关联和区别呢,咱们前⾯讲了MVC模式, 后端会返回视图, 这是早期时的概念

随着互联网的发展, 目前项目开发流行"前后端分离"模式, Java 主要是用来做后端项目的开发, 所以也就不再处理前端相关的内容了
因此 MVC 的概念也逐渐发生了变化, View 不再返回视图, 而是返回显示视图时需要的数据.所以前⾯使⽤的 @RestController 其实是返回的数据.
其实 @RestController = @Controller + @ResponseBody
所以如果想返回视图的话, 只需要用 @Controller 就可以了
我们上⾯讲到, @ResponseBody 表示返回数据.
@Controller
@ResponseBody
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
}
加上 @ResponseBody 注解, 该方法就会把 “/index.html” 当做⼀个数据返回给前端.
运行:http://127.0.0.1:8080/index

@ResponseBody 既是类注解, ⼜是方法注解
也就是说: 在类上添加 @ResponseBody 就相当于在所有的方法上添加了 @ResponseBody 注解.同样, 如果类上有 @RestController 注解时:表示所有的方法上添加了 @ResponseBody 注解
如果⼀个类的方法里, 既有返回数据的, 又有返回页面的, 就把 @ResponseBody 注解添加到需要返回页面的方法上即可.
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
@RequestMapping("/returnData")
@ResponseBody
public String returnData(){
return "该⽅法返回数据";
}
}
多个注解时, 没有先后顺序, 先写哪个都可以
运行程序, 浏览器响应结果如下: http://127.0.0.1:8080/returnData

如果去掉 @ResponseBody 注解, 程序会报404错误,程序会认为需要返回的是视图, 根据内容去查找文件, 但是查询不到, 路径不存在, 报404

后端返回数据时, 如果数据中有 HTML 代码, 也会被浏览器解析
@RequestMapping("/returnHtml")
@ResponseBody
public String returnHtml() {
return "Hello,HTML~
";
}
运行程序, 浏览器响应结果如下: http://127.0.0.1:8080/returnHtml

通过 Fiddler 观察响应结果, Content-Type 为 text/html

响应中的 Content-Type 常见取值有以下几种:
Spring MVC 也可以返回 JSON
后端方法返回结果为对象:
@RequestMapping("/returnJson")
@ResponseBody
public HashMap<String, String> returnJson() {
HashMap<String, String> map = new HashMap<>();
map.put("Java", "Java Value");
map.put("MySQL", "MySQL Value");
map.put("Redis", "Redis Value");
return map;
}
运行程序, 浏览器响应结果如下: http://127.0.0.1:8080/returnJson

过 Fiddler 观察响应结果, Content-Type 为 application/json

Spring MVC 会根据我们方法的返回结果自动设置响应状态码, 程序员也可以手动指定状态码,状态码可以通过 Spring MVC 的内置对象 HttpServletResponse 提供的方法来进行设置
@RequestMapping(value = "/setStatus")
@ResponseBody
public String setStatus(HttpServletResponse response) {
response.setStatus(401);
return "设置状态码成功";
}
运行程序, 浏览器响应结果如下: http://127.0.0.1:8080/setStatus

通过 Fiddler 来观察设置的结果:

Http 响应报头也会向客户端传递⼀些附加信息,这些信息通过 @RequestMapping 注解的属性来实现
先来看 @RequestMapping 的源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
我们通过设置 produces 属性的值, 设置响应的报头 Content-Type
@RequestMapping(value = "/returnJson2",produces = "application/json")
@ResponseBody
public String returnJson2() {
return "{\"success\":true}";
}
运行程序, 浏览器响应结果如下:http://127.0.0.1:8080/returnJson2

通过 Fiddler 来观察设置的结果:

如果不设置 produces , 方法返回结果为 String 时, Spring MVC 默认返回类型是 text/html.
设置其他 Header 的话, 需要使⽤ Spring MVC 的内置对象 HttpServletResponse 提供的方法来进行设置
@RequestMapping(value = "/setHeader")
@ResponseBody
public String setHeader(HttpServletResponse response) {
response.setHeader("MyHeader","MyHeaderValue");
return "设置Header成功";
}
void setHeader(String name, String value) 设置⼀个带有给定的名称和值的 header. 如果 name已经存在, 则覆盖旧的值.
运行程序, 浏览器响应结果如下: http://127.0.0.1:8080/setHeader

通过 Fiddler 来观察设置的结果:

结合上述内容, 我们可以做⼀些小案例,主要掌握知识点:
需求: 输入两个整数, 点击 “点击相加” 按钮, 显示计算结果


首先进行需求分析:加法计算器功能, 对两个整数进行相加, 需要客户端提供参与计算的两个数, 服务端返回这两个整数计算的结果,基于以上分析, 我们来定义接口:
接口定义:
请求路径:calc/sum
请求⽅式:GET/POST
接⼝描述:计算两个整数相加
请求参数:
| 参数名 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| num1 | Integer | 是 | 参与计算的第⼀个数 |
| num2 | Integer | 是 | 参与计算的第⼆个数 |
响应数据:
Content-Type: text/html
响应内容: 计算机计算结果: 8
@RestController
@RequestMapping("/calc")
public class CalcController {
@RequestMapping("/sum")
public String sum(Integer num1,Integer num2){
Integer sum = num1+num2;
return "计算机计算结果: "
+sum+"";
}
}
<form action="calc/sum" method="post">
<h1>计算器h1>
数字1:<input name="num1" type="text"><br>
数字2:<input name="num2" type="text"><br>
<input type="submit" value=" 点击相加">
form>
启动服务, 运行并测试

需求:


创建新项目, 引入对应依赖, 把前端页面放在项目中
需求分析:图书管理系统是⼀个相对较大一点的案例, 咱们先实现其中的⼀部分功能.


根据需求可以得知, 后端需要提供两个接口
接口定义:
[URL]
POST /user/login
[请求参数]
name=admin&password=admin
[响应]
true //账号密码验证成功
false//账号密码验证失败
[URL]
POST /book/getList
[请求参数]
⽆
[响应]
返回图书列表
[
{
"id": 1,
"bookName": "活着",
"author": "余华",
"count": 270,
"price": 20,
"publish": "北京⽂艺出版社",
"status": 1,
"statusCN": "可借阅"
},
...
]
| 参数名 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| id | Integer | 是 | 图书ID |
| bookName | String | 是 | 图书名称 |
| author | String | 是 | 作者 |
| count | Integer | 是 | 数量 |
| price | Double | 是 | 定价 |
| publish | String | 是 | 图书出版社 |
| status | Integer | 是 | 图书状态 |
| statusCN | String | 是 | 图书状态中文含义 |
@Data
public class BookInfo {
//图书ID
private Integer id;
//书名
private String bookName;
//作者
private String author;
//数量
private Integer count;
//定价
private BigDecimal price;
//出版社
private String publish;
//状态 0-⽆效 1-允许借阅 2-不允许借阅
private Integer status;
private String statusCN;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
}
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public boolean login(String name, String password, HttpSession session){
//账号或密码为空
if (!StringUtils.hasLength(name) || !StringUtils.hasLength(password)){
return false;
}
//模拟验证数据, 账号密码正确
if("admin".equals(name) && "admin".equals(password)){
session.setAttribute("userName",name);
return true;
}
//账号密码错误
return false;
}
}
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/getList")
public List<BookInfo> getList(){
//获取数据
List<BookInfo> books = mockData();
//处理⻚⾯展⽰
for (BookInfo book:books){
if (book.getStatus()==1){
book.setStatusCN("可借阅");
}else {
book.setStatusCN("不可借阅");
}
}
return books;
}
/**
* 数据Mock 获取图书信息
*
* @return
*/
public List<BookInfo> mockData() {
List<BookInfo> books = new ArrayList<>();
for (int i = 0; i < 5; i++) {
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("书籍" + i);
book.setAuthor("作者" + i);
book.setCount(i * 5 + 3);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社" + i);
book.setStatus(1);
books.add(book);
}
return books;
}
}
在这个实例中数据采⽤ mock(假数据) 的方式, 实际数据应该从数据库中获取
添加登录处理逻辑
<script src="js/jquery.min.js"></script>
<script>
function login() {
$.ajax({
type: "post",
url: "/user/login",
data: {
name: $("#userName").val(),
password: $("#password").val()
},
success: function (result) {
if (result) {
location.href = "book_list.html";
} else {
alert("账号或密码不正确!");
}
}
});
}
</script>
删除前端伪造的代码, 从后端获取数据并渲染到页面上
function getBookList() {
$.ajax({
type: "get",
url: "/book/getList",
success: function(result) {
console.log(result);
if (result != null) {
var finalHtml = "";
for (var book of result) {
finalHtml += '';
finalHtml += ' ';
finalHtml += '' + book.id + ' ';
finalHtml += '' + book.bookName + ' ';
finalHtml += '' + book.author + ' ';
finalHtml += '' + book.count + ' ';
finalHtml += '' + book.price + ' ';
finalHtml += '' + book.publish + ' ';
finalHtml += '' + book.statusCN + ' ';
finalHtml += ' ';
finalHtml += ' ';
}
$("tbody").html(finalHtml);
}
}
});
}
访问: http://127.0.0.1:8080/login.html
输入账号密码: admin admin, 登录成功, 跳转到图书列表页,界⾯展示:

Lombok 是⼀个 Java 工具库,通过添加注解的方式,简化 Java 的开发.
下面来简单来学习下它的使用:
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
lombok通过⼀些注解的方式, 可以帮助我们消除⼀些冗长代码, 使代码看起来简洁⼀些,比如之前的 Person 对象 就可以改为
@Data
public class Person {
private int id;
private String name;
private String password;
}
@Data 注解会帮助我们⾃动⼀些方法, 包含 getter/setter, equals, toString 等
加了 @Data 注解之后, Idea 反编译的 class ⽂件,不是真正的字节码文件,而是 Idea 根据字节码进行反编译后的文件
反编译是将可执行的程序代码转换为某种形式的高级编程语言, 使其具有更易读的格式. 反编译是⼀种逆向工程,它的作用与编译器的作用相反


可以看出来, lombok 是⼀款在编译期生成代码的工具包.


4. 更多使用
如果觉得 @Data 比较粗暴(生成方法太多), lombok 也提供了⼀些更精细粒度的注解
| 注解 | 作用 |
|---|---|
| @Getter | 自动添加 getter 方法 |
| @Setter | 自动添加 setter 方法 |
| @ToString | 自动添加 toString 方法 |
| @EqualsAndHashCode | 自动添加 equals 和 hashCode 方法 |
| @NoArgsConstructor | 自动添加无参构造方法 |
| @AllArgsConstructor | 自动添加全属性构造方法,顺序按照属性的定义顺序 |
| @NonNull | 属性不能为 null |
| @RequiredArgsConstructor | 自动添加必需属性的构造方法,final + @NonNull 的属性为必需 |
@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor+@NoArgsConstructor
上述引入 lombok 依赖, 需要去找 lombok 的坐标,接下来介绍更简单引入依赖的⽅式



注意:不是所有依赖都可以在这里添加的, 这个界面和 SpringBoot 创建项目界⾯⼀样,依赖不在这里的, 还需要去 Maven 仓库查找坐标, 添加依赖.
通过上面的练习, 我们学习了 Spring MVC 简单功能的开发, 但是我们也发现了⼀些问题:目前我们程序的代码有点 “杂乱”, 然而当前只是 “⼀点点功能” 的开发. 如果我们把整个项目功能完成呢?代码会更加的 “杂乱无章”
基于此, 咱们接下来学习应用分层.

阿里开发手册中, 关于工程结构部分, 定义了常见工程的应用分层结构:

应用分层是⼀种软件开发设计思想, 它将应用程序分成N个层次, 这N个层次分别负责各自的职责, 多个层次之间协同提供完整的功能,根据项目的复杂度, 把项目分成三层, 四层或者更多层
在最开始的时候,为了让项目快速上线,我们通常是不考虑分层的.
但是随着业务越来越复杂,大量的代码混在⼀起,会出现逻辑不清晰、各模块相互依赖、代码扩展性差、改动⼀处就牵⼀发而动全身等问题. 所以我们需要对应用进行分层
咱们上⼀节中学习的 MVC, 就是把整体的系统分成了 Model(模型), View(视图)和Controller(控制器)三个层次
也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是⼀种标准的软件分层架构。

目前现在更主流的开发方式是 “前后端分离” 的方式, 后端开发工程师不再需要关注前端的实现,
所以对于Java后端开发者, ⼜有了⼀种新的分层架构: 把整体架构分为表现层、业务逻辑层和数据层. 这种分层⽅式也称之为"三层架构".
可以看到, 咱们前面的代码, 并不符合这种设计思想, 我们将所有的代码堆砌在⼀起

按照上面的层次划分, Spring MVC 站在后端开发人员的角度上, 也进行了支持, 把上面的代码划分为三个部分:
这三个部分, 在Spring的实现中, 均有体现:

Spring MVC 是针对 Web 应用程序的一个具体框架,而三层架构是一种通用的软件架构模式,Spring MVC 是在表示层的基础上实现了 MVC 设计模式,而三层架构涵盖了整个应用程序,包括表示层、业务逻辑层和数据访问层。

我们使用上⾯的分层思想, 来对代码进行改造

package com.example.demo.controller;
import com.example.demo.model.BookInfo;
import com.example.demo.service.BookService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/getList")
public List<BookInfo> getList(){
BookService bookService = new BookService();
//获取数据
List<BookInfo> books = bookService.getBookList();
return books;
}
}
import com.example.demo.dao.BookDao;
import com.example.demo.model.BookInfo;
import java.util.List;
public class BookService {
public List<BookInfo> getBookList(){
BookDao bookDao = new BookDao();
List<BookInfo> books = bookDao.mockData();
for (BookInfo book:books){
if (book.getStatus()==1){
book.setStatusCN("可借阅");
}else {
book.setStatusCN("不可借阅");
}
}
return books;
}
}
import java.util.List;
import java.util.Random;
public class BookDao {
/**
* 数据Mock 获取图书信息
* @return
*/
public List<BookInfo> mockData(){
List<BookInfo> books = new ArrayList<>();
for (int i=0;i<5;i++){
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("书籍"+i);
book.setAuthor("作者"+i);
book.setCount(i*5+3);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社"+i);
book.setStatus(1);
books.add(book);
}
return books;
}
}
常见命名命名风格介绍