SpringBootWeb入门
Spring官网:https://spring.io/
先安装并配置Java17:
1.去Oracle官网下载Java17
2.解压到无空格、无中文的目录
3.配置系统环境变量
3.1新建系统变量JAVA_HOME,值为JDK17的解压路径
3.2编辑系统变量Path,把%JAVA_HOME%\bin移到最顶部,避免和原来的Java11冲突
4.验证:打开新的cmd窗口,输入java -version
再像4-Maven中一样对IDEA进行全局配置即可。
由于我们使用的是社区版,它本身不支持直接创建Spring Boot项目,我们可以用Maven手动创建。我们新建一个Maven模块,再在其pom.xml中添加Spring Boot的父依赖和起步依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
编写启动类,添加@SpringBootApplication注解
package com.tkevinjd;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
运行,观察控制台日志可以看到,Tomcat服务器正常初始化并启动在8080端口,SpringBoot上下文加载完成。这个Application启动类必须保持运行状态,SpringBoot项目以及内置的Tomcat服务器才能正常提供Web服务。创建类要在启动类的软件包下,才能进行Web开发。如果遇到特殊情况需要扫描根包外的类,就在启动类的注解后加上scanBasePackages属性,在其后面指定要扫描的包即可。后续我们可以配置Spring Boot热部署,配置后修改了Java代码/配置文件,IDEA会自动重启项目,无需手动点运行。
接着,我们在resources目录下创建static与templates文件夹与application.properties. static文件夹用来存放静态资源(浏览器可以直接访问),例如前端的html、css、js文件与图片视频等。templates文件夹存放模板文件,需要配合模板引擎使用,例如动态渲染的页面。application.properties是SpringBoot的核心配置文件,用来自定义项目配置,比如修改Tomcat端口、配置数据库连接、设置日志级别等。因为我们使用的是Maven手动创建SpringBoot项目,所以我们要自己设置SpringBoot的标准化资源目录结构,所有SpringBoot项目都会按照这个规范来。另外,这些目录的名字不能改,static和templates是SpringBoot的约定式目录名,稍微进行一点改动都不行,例如改成Static和模板。因为框架会自动去这两个目录找对应资源,改动名字就找不到了。不过application.properties可以改成application.yml,SpringBoot是支持的。
需求:基于SpringBoot开发一个web应用,浏览器发起请求/hello后,给浏览器返回一个字符串"Hello XXX"
方法:定义HelloController类,添加方法hello,并添加注解@RestController
package com.tkevinjd;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController //标识当前类是一个请求处理类
public class HelloController {
@RequestMapping("/hello") //标识当前方法处理的请求路径
public String hello(String name) {
//给前端相应的字符串数据
return "Hello, " + name + "!";
}
}
运行启动类时,使用Debug运行便于我们代码排查。然后我们打开浏览器,在问号后面输入请求参数。就可以看到前端返回的界面。
我们来分析一下,为什么运行启动类的main方法,就直接将我们的web应用程序启动了呢?
org.springframework.boot:spring-boot-stater-web:3.2.0
org.springframework.boot:spring-boot-stater-test:3.2.0
这两个依赖的名字类似,被称作起步依赖,它们两个分别包含了web应用开发所需要的常见依赖与单元测试所需要的常见依赖。引入它们两个即可,因为maven有依赖传递。在web开发的起步依赖中可以看到spring-boot-stater-tomcat,它是一个web服务器。当我们运行启动类,运行它的main方法时,实际上是将引入进来的tomcat服务器启动了,所以我们也可以在控制台看到tomcat的一些初始化的日志。服务器运行起来后,我们的项目代码都会部署到这个服务器当中。
http
全称:Hyper Text Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。客户端浏览器需要访问服务器端来获取数据,那么此时浏览器就需要给服务器发送请求,服务器接收到请求后会对请求进行处理,再给浏览器响应对应的数据。那么,浏览器在给服务器发送请求的时候就要携带相关的请求数据,要告诉服务器我需要什么数据以及我需要的数据格式是什么样的。服务器接收到这些请求后就需要对这些请求数据进行解析,而服务器要想能够成功解析出来所携带过来的请求,那它就要知道浏览器发送过来的请求数据到底长什么样子、具体的数据格式是什么样子的、具体每一项代表什么含义,否则无法解析。也就是说服务器与浏览器之间需要建立好一个约定,才能交流。这就像不同国家之间的人如果不通语言也是无法交流的一样。
在开发者工具的网络部分就可以看到请求数据与响应数据的真实模样。
HTTP的特点:
1.基于TCP:面向连接、安全
2.基于请求-响应模型:一次请求对应一次响应
3.HTTP协议是无状态的协议:对于事务处理没有记忆能力,每次请求-响应都是独立的。优点是速度快,缺点是多次请求间不能共享数据。
请求数据格式
请求数据由三部分组成:
1.请求行:请求数据第一行(请求方式、资源路径、协议)
2.请求头:第二行开始,格式为key:value
| key | 含义 |
|---|---|
| Host | 请求的主机名 |
| User-Agent | 浏览器版本 |
| Accept | 表示浏览器能接收的资源类型,如text/*,image/*或*/*表示所有 |
| Accept-Language | 表示浏览器偏好的语言,服务器可以据此返回不通语言的网页 |
| Accept-Encoding | 表示浏览器可以支持的压缩类型,例如gzip,deflate等 |
| Content-Type | 请求主体的数据类型 |
| Content-Length | 请求主体的大小(单位:字节) |
3.请求体:POST请求,存放请求参数。GET方式没有请求体,因为请求参数在请求行中,请求大小在浏览器中有限制。
请求数据获取
Web服务器(例如Tomcat)对HTTP协议的请求数据进行解析,并进行了封装(HttpServletRequest),在调用controller方法时传递给了该方法。
package com.tkevinjd;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.http.HttpClient;
@RestController
public class RequestController {
@RequestMapping("/request")//标识当前方法处理的请求路径
public String request(HttpServletRequest request) {
//在方法中我们就可以通过request对象获取请求数据的各个部分了
//1.获取请求方式
String method = request.getMethod();
System.out.println("请求方式:" + method);
//2.获取请求url地址
String requestURL = request.getRequestURL().toString();
System.out.println("请求url地址:" + requestURL);
String requestURI = request.getRequestURI();
System.out.println("请求uri地址:" + requestURI);
//3.获取请求协议
String protocol = request.getProtocol();
System.out.println("请求协议:" + protocol);
//4.获取请求参数 -name
String name = request.getParameter("name");
System.out.println("请求参数-name:" + name);
//5.获取请求头 -Accept
String accept = request.getHeader("Accept");
System.out.println("请求头-Accept:" + accept);
//6.给前端返回
return "OK";
}
}
控制台内容:
请求方式:GET
请求url地址:http://localhost:8080/request
请求uri地址:/request
请求协议:HTTP/1.1
请求参数-name:mio
请求头-Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
响应数据格式
1.响应行(协议、状态码、描述)
2.响应头:第二行开始,格式key:value
3.响应体:最后一部分,存放响应数据
| 状态码 | 说明 |
|---|---|
| 1xx | 响应中-临时状态码,表示请求已经接收,告诉客户端应该继续请求或者如果它已经完成则忽略它 |
| 2xx | 成功-表示请求已经被成功接收,处理已完成 |
| 3xx | 重定向-重定向到其它地方,让客户端再发起一次请求以完成整个处理 |
| 4xx | 客户端错误-处理发生错误,责任在客户端。如请求了不存在的资源、客户端未被授权、禁止访问等 |
| 5xx | 服务器错误-处理发生错误,责任在服务端。如程序抛出异常等。 |
重定向:浏览器向服务器A发起请求,A发现浏览器请求的数据不在自己这里,在服务器B处,于是它会给浏览器返回一个3xx状态码,以及浏览器B的地址Location,浏览器获取到状态码与Location后,会向服务器B发起请求,进而得到数据。这个过程会有多次请求,但用户是无感知的。例如我们可以用http://www.baidu.com搜索百度,会发现浏览器发起了两次请求,第一次请求的状态码是3开头的,第二次则是200,表示成功。
具体:
| 状态码 | 英文描述 | 解释 |
|---|---|---|
| 200 | OK | 客户端请求成功,即处理成功,这是我们最想看到的状态码 |
| 302 | Found | 指示所请求的资源已移动到由Location响应头给定的URL,浏览器会自动重新访问 |
| 304 | Not Modified | 告诉客户端,你请求的资源自上次取得后,服务端并未更改 |
| 400 | Bad Request | 客户端请求有语法错误,不能被服务器理解 |
| 403 | Forbidden | 服务器收到请求,但是拒绝提供服务,例如没有权限访问相关资源 |
| 404 | Not Found | 请求资源不存在,一般是URL输入有误,或者网站资源被删除了 |
| 405 | Method Not Allowed | 请求方式有误,例如应该用GET方式请求的资源使用了POST |
| 428 | Precodition Required | 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
| 429 | Too Many Requests | 指定用户在给定时间内发送了太多请求 |
| 431 | Request Header Fields Too Large | 请求头太大,服务器不愿意处理请求 |
| 500 | Internal Server Error | 服务器发生不可预期的错误,赶紧去看日志 |
| 503 | Service Unavailable | 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
状态码大全:https://cloud.tencent.com/developer/chapter/13553
| 常见响应头 | 说明 |
|---|---|
| Content-Type | 表示该响应内容的类型,例如text/html,application/json |
| Content-Length | 表示该响应内容的长度(字节数) |
| Content-Encoding | 表示该响应压缩算法,例如gzip |
| Cache-Control | 指示客户端应如何缓存,例如max-age=300表示可以最多缓存300s |
| Set-Cookie | 告诉浏览器为当前页面所在的域设置Cookie |
响应数据设置
方式一:基于HttpServletResponse封装
@RestController
public class ResponseController {
@RequestMapping("/reponse")
public void response(HttpServletResponse response) throws IOException {
//1.设置响应状态码
response.setStatus(HttpServletResponse.SC_OK);
//2.设置响应头
response.setHeader("name", "tkevinjd");
//3.设置响应体
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("<h1>hello response</h1>");
}
}
方式二:基于ResponseEntity封装
@RestController
public class ResponseController {
//Spring中提供的方式
@RequestMapping("/response")
public ResponseEntity<String> response(HttpServletResponse response) throws IOException {
return ResponseEntity.status(401)
.header("name","mugi")
.body("<h1>hello kon<h1>");
}
}
注意:响应状态码和响应头如果没有特殊要求的话,通常不手动设定。服务器会根据请求处理的逻辑,自动设置响应状态码和响应头。
案例:基于SpringBoot开发一个Web应用程序,完成用户列表的渲染展示
我们在前端页面中基于axios发送异步请求,请求我们服务器端所开发的这个Web应用程序,请求路径为http://localhost:8080/list,请求到达服务器端后,服务器端接收这个请求并且读取数据。我们将数据定义在一份单独的文件中。服务器端将数据读取出来后进行数据的组装。将组装好的数据返回给前端。最后在前端页面中,就可以通过Vue当中的指令,将JSON格式的数据渲染展示在页面当中。
我们先安装Lomcok插件。再在pom.xml进行依赖的配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tkevinjd</groupId>
<artifactId>spring-boot-web-01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.27</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
</project>
再将user.txt放在resources目录下。
接着封装一个实体类User
package com.tkevinjd.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
//用户信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;//使用包装类,因为如果是基本类的话,0是默认值,无法判断是业务上的0还是使用了默认值
private String username;
private String password;
private String name;
private Integer age;
private LocalDateTime updateTime;
}
中途出了点小问题,我的user.txt中的id域username的分隔符打成了英文句号而不是英文逗号,导致后端解析数据时出错。这点需要注意,不要打错分隔符。如果发现浏览器将年月日与时分秒之间的空格显示为大写T,那么可以对user类进行如下修改:
//用户信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;//使用包装类,因为如果是基本类的话,0是默认值,无法判断是业务上的0还是使用了默认值
private String username;
private String password;
private String name;
private Integer age;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
UserController类:
package com.tkevinjd.controller;
import com.tkevinjd.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
//用户信息的Controller
@RestController
public class UserController {
@RequestMapping("/list")
public List<User> list() throws IOException {
//1.加载并读取user.txt文件,获取用户数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = new ArrayList<>();
if(in != null) {
//使用BufferedReader逐行读取
BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line = null;
while((line = br.readLine()) != null) {
lines.add(line);
}
br.close();
} else {
System.err.println("未找到user.txt文件");
return null;
}
//2.解析用户信息,封装为user对象 -> list集合
//这里直接将list集合返回
//3.给前端返回json数据(直接将集合响应给前端即可,因为服务器在响应集合时会将其转换为json格式的数据)
return lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).toList();
}
}
user.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户列表数据</title>
<style>
table {
border-collapse: collapse;
width: 50%;
margin-top:20px;
border:1px solid #ccc;
text-align: center;
font-size:14px;
}
tr {
height: 40px;
}
th, td {
border: 1px solid #ccc;
}
thead {
background-color: #e8e8e8;
}
h1 {
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div id="app">
<!-- 定义一个表格,包括6列,分别是:ID、用户名、密码、姓名、年龄、更新时间 -->
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>密码</th>
<th>姓名</th>
<th>年龄</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in userList" :key="index">
<td>{
{ user.id }}</td>
<td>{
{ user.username }}</td>
<td>{
{ user.password }}</td>
<td>{
{ user.name }}</td>
<td>{
{ user.age }}</td>
<td>{
{ user.updateTime }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 引入axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 使用全局构建的 Vue(避免模块导入问题) -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const {
createApp } = Vue;
createApp({
data() {
return {
userList: []
};
},
methods: {
async search() {
const result = await axios.get('/list');
this.userList = result.data;
}
},
mounted() {
this.search();
}
}).mount('#app');
</script>
</body>
</html>
user.txt:(放在static目录下)
1,HirasawaYui,1234567890,平泽唯,17,2026-01-25 15:05:35
2,AkiyamaMio,1234567891,秋山澪,17,2026-01-23 08:41:33
3,TainakaRitsu,1234567892,田井中律,17,2026-01-24 19:33:02
4,NakanoAzusa,1234567893,中野梓,16,2026-01-23 12:07:11
5,KotobukiTsumugi,1234567894,琴吹紬,17,2026-01-22 21:05:03
6,HirasawaUi,1234567895,平泽忧,15,2026-01-22 07:06:22
7,ManabeNodoka,1234567896,真锅和,17,2026-01-21 09:00:07
8,YamanakaSawako,1234567897,山中佐和子,24,2026-01-21 10:06:11
运行服务器后可以访问http://localhost:8080/user.html看界面,或访问http://localhost:8080/list查看原始数据。梳理一下整个运行的流程:
1.启动tomcat服务器
2.访问/user.html,即请求访问服务器中运行的user.html这个静态界面
3.收到GET /user.html时,Spring的资源处理映射(ResourceHandler)会优先匹配并直接返回静态文件内容,无需进入UserController
4.浏览器加载user.html后,页面内的Vue+axios会发起请求到/list,触发/list的流程
5.页面内Vue的钩子函数mounted()被调用,mounted里进一步调用search函数
6.search()用axios发起GET /list
7.tomcat接收并将请求封装为HttpServletRequest,按Servlet映射将请求传给DispatcherServlet(Spring MVC的前端控制器),DispatcherServlet通过HandlerMapping(注解映射)查找@RequestMapping("/list")对应的UserController.list()
8.HandlerAdapter调用UserController.list(),在list方法中读取类路径资源user.txt,控制器返回List<User>,DispatcherServlet使用合适的HttpMessageConverter将返回的List序列化为JSON,这里需要注意的是,LocalDateTime的默认序列化规则是ISO-8601(原本的"2026-01-21 15:05:35"会变成"2026-01-21T15:05:35"),我们需要在User.updateTime上使用@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")指定格式,消除那个T
9.序列化后的JSON写回HttpServletResponse,tomcat将响应发送回客户端,http请求完成
关于第八点的补充:@RestController触发把返回值当响应体处理,它等同于@Controller+@ResponseBody,它告诉Spring把控制器方法的返回值直接写入响应体而不是当作视图名去解析。请求经过DispatcherServlet,找到对应的处理器并调用方法后,带有@ResponseBody的返回值由RequestResponseBodyMethodProcessor处理,它使用注册的HttpMessageConverter将对象/集合序列化为JSON,然后写入HttpServletResponse
这个流程也解释了,为什么我们会先看到空模板,过了零点几秒才看到真实的数据。
三层架构
现在我们将所有逻辑都写在了list方法中,复用性与可维护性比较差。。
在list方法中,可以将代码的功能分为三个部分:数据访问、逻辑处理、接收请求并响应数据。
三层架构:
Controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
Service:业务逻辑层,处理具体的业务逻辑
Dao:数据访问层(Data Access Object),负责数据访问操作,包括数据的crud.可能从文件中读取数据,也可能从数据库读取,也可能从网络中读取
不难发现流程是自上而下的。浏览器发起请求后,到达控制层(因为它负责接收前端发送的请求), 再到处理逻辑的业务逻辑层,而业务逻辑层需要先拿到数据,所以再到数据访问层。接着数据逆向往上走,最终交给前端。
原本的controller包不用动,新建dao包和service包,在这两个包下创建UserDao和UserService接口,再在这两个包下创建impl.UserDaoImpl和impl.UserServiceImpl类,这是规范写法。
UserDao.java
package com.tkevinjd.dao;
import java.io.IOException;
import java.util.List;
public interface UserDao {
//加载用户数据
public List<String> findAll() throws IOException;
}
UserDaoImpl.java
package com.tkevinjd.dao.impl;
import com.tkevinjd.dao.UserDao;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class UserDaoImpl implements UserDao {
//带impl后缀的表示实现类
@Override
public List<String> findAll() throws IOException {
//1.加载并读取user.txt文件,获取用户数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = new ArrayList<>();
if(in != null) {
//使用BufferedReader逐行读取
BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line = null;
while((line = br.readLine()) != null) {
lines.add(line);
}
br.close();
} else {
System.err.println("未找到user.txt文件");
return null;
}
return lines;
}
}
UserService.java
package com.tkevinjd.service;
import com.tkevinjd.pojo.User;
import java.io.IOException;
import java.util.List;
public interface UserService {
//查询所有用户
public List<User> findAll() throws IOException;
}
UserServiceImpl.java
package com.tkevinjd.service.impl;
import com.tkevinjd.dao.UserDao;
import com.tkevinjd.dao.impl.UserDaoImpl;
import com.tkevinjd.pojo.User;
import com.tkevinjd.service.UserService;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public List<User> findAll() throws IOException {
//1.调用dao,获取数据
List<String> lines = userDao.findAll();
//2.解析用户信息,封装为user对象 -> list集合
return lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).toList();
}
}
UserController.java
package com.tkevinjd.controller;
import com.tkevinjd.pojo.User;
import com.tkevinjd.service.UserService;
import com.tkevinjd.service.impl.UserServiceImpl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.util.List;
//用户信息的Controller
@RestController
public class UserController {
private UserService userService = new UserServiceImpl();
@RequestMapping("/list")
public List<User> list() throws IOException {
//1.调用userService获取数据
//2.返回json
//合并为一步
return userService.findAll();
}
}
分层解耦
耦合:衡量软件中各个层/各个模块的依赖关联程度
内聚:软件中各个功能模块内部的功能联系
软件设计原则:高内聚低耦合。
我们将项目更改为三层架构后,在UserControll里我们new了UserService对象,在UserService里我们new了UserDao对象,这增强了耦合性,所以我们不能new对象。但是如果只声明,那么值就是null,调用list方法会出现空指针异常。我们可以使用容器来存放对象,需要用的时候就从容器里拿。这里涉及到Spring中的重要概念:
控制反转
Inversion Of Control,简称IOC,对象的创建控制权由程序自身转移到外部(容器),这种思想被称为控制反转
依赖注入
Dependency Injection,简称DI,容器为应用程序提供运行时所依赖的资源,称之为依赖注入
Bean对象
IOC容器中创建、管理的对象,称之为Bean
实践步骤:
1.将Dao与Service层的实现类,交给IOC容器管理(Component注解或其衍生注解)
2.为Controller及Service注入运行时所依赖的对象(Autowired注解)
IOC详解
| 注解 | 说明 | 位置 |
|---|---|---|
| @Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
| @Controller | @Component的衍生注解 | 标注在控制层类上 |
| @Service | @Component的衍生注解 | 标注在业务层类上 |
| @Repository | @Component的衍生注解 | 标注在数据访问层类上(由于mybatis整合,较少用) |
声明bean的时候,可以通过注解的value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
前面声明的bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描。该注解虽然没有显式配置,但实际上已经包含在了启动类生命注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包。
DI详解
基于@Autowired进行依赖注入的常见方式有如下三种:
1.属性注入
优点:代码简洁
缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性
@RestController
public class UserController {
@Autowired
private UserService userService;
//...
}
2.构造函数注入
优点:能清晰地看到类的依赖关系、提高了代码的安全性
缺点:代码繁琐、如果构造含参数过多,可能会导致构造函数臃肿
注意:如果只有一个构造函数,@Autowired注解可以省略
@RestController
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService
}
}
3.setter注入
优点:保证了类的封装性,依赖关系更清晰
缺点:需要额外编写set方法,增加了代码量
@RestController
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService
}
}
@Autowired注解默认是按照类型进行注入的,如果存在多个相同类型的bean,将会报错。
解决方案一:@Primary
@Primary //优先注入Primary标注的类
@Service
public class UserServiceImpl implements UserService {
@Override
public List<User> list() {
//...
}
}
解决方案二:@Qualifier
@RestController
public class UserController {
@Autowired
@Qualifier("userServiceImpl")//填bean的名字,前面说过如果没有指定,就是类名首字母小写
private UserService userService;
}
解决方案三:@Resource
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
}
@Autowired是Spring框架提供的注解,而@Resource是JavaEE规范提供的
@Autowired默认按照类型注入,而@Resource默认按照名称注入