JavaWeb_SpringBootWeb

  • BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
    • 优点:维护方便
    • 缺点:体验一般
  • CS架构:Client/Server,客户端/服务器架构模式。需要单独开发维护客户端。
    • 优点:体验不错
    • 缺点:开发维护麻烦

SprintBootWeb

入门程序

需求:基于SpringBoot的方式开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串 “Hello xxx ~”。

开发步骤

第1步:创建SpringBoot工程,并勾选Web开发相关依赖
第2步:定义HelloController类,添加方法hello,并添加注解

创建Spring Boot 工程

由于我的IDEA版本是社区版,没有办法直接从插件处下载Sprintboot插件,所以要采取一些其他方法。

方法2和方法3都比较方便。
方法2:
使用Spring initializr插件来创建。

  • 发现这个插件也变成专业版了!!为什么!

方法3:通过maven来创建
需要修改pom.xml来添加依赖:

  • 需要新增父项目配置
1
2
3
4
5
6
<parent>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- 使用更新的稳定版本 -->
<version>2.7.18</version>
</parent>

用于继承Springboot的默认配置,统一依赖管理版本

  • 加入Spring bott核心依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

在这里引入依赖坐标,提供了web开发所需的全部依赖(Spring mvc,内嵌tomcat等)

  • 引入Spring boot maven插件
1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

用于打包可执行jar,允许spring boot应用,热部署支持

  • 引入测试依赖更新
1
2
3
4
5
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

方便我们使用JUnit5进行Spring boot测试

在主类App.java中import:

1
2
package org.example; 
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

运行成功:

完整的SpringBoot项目还需要添加资源文件

定义HelloController类,添加方法hello

运行测试:
打开浏览器,输入 localhost:8080/hello?name=土豆
可以看到

回顾一下

在入门程序中,我们发现只需要一个main方法就可以将web应用启动起来,然后可以打开浏览器访问了。
why?
因为我们在创建springboot项目的时候,选择了web开发的起步依赖 spring-boot-starter-web。而spring-boot-starter-web依赖,又依赖了spring-boot-starter-tomcat,由于maven的依赖传递特性,那么在我们创建的springboot项目中也就已经有了tomcat的依赖,这个其实就是springboot中内嵌的tomcat。

  • spring-boot-starter-web:包含了web应用开发所需要的常见依赖。
  • spring-boot-starter-test:包含了单元测试所需要的常见依赖。

HTTP协议

概述

HTTP:Hyper Text Transfer Protocol(超文本传输协议),规定了浏览器与服务器之间数据传输的规则。(应用层协议)

  • http协议又分为请求协议和相应协议
  • 规定数据传输的格式:请求数据+响应数据

特点

  • 基于TCP协议:面向连接,安全
    • TCP是面向连接的协议,需要三次握手
  • 基于请求-响应模型:一次请求对应一次响应
  • http协议是无状态协议:对于数据没有记忆能力,每次请求-响应都是独立的
    • 我们为了有“记忆”引入了cookie等技术

HTTP请求协议

请求协议就是浏览器将数据以请求格式发送到服务器。包括:请求行、请求头、请求体。

请求方式:GET/POST

GET方式的请求协议:

  • 请求行:
    • 请求方式:GET
    • 资源路径:/brand/findAll?name=OPPO&status=1
      • 请求路径:/brand/findAll
      • 请求参数:name=OPPO&status=1
        • 请求参数是以key=value形式出现
        • 多个请求参数之间使用&连接
      • 请求路径和请求参数之间使用?连接
    • 协议/版本:HTTP/1.1
  • 请求头:key:value
    • http是个无状态的协议,所以在请求头设置浏览器的一些自身信息和想要响应的形式。
请求头 含义
Host 表示请求的主机名
User-Agent 浏览器版本。 例如:Chrome浏览器的标识类似Mozilla/5.0 …Chrome/79 ,IE浏览器的标识类似Mozilla/5.0 (Windows NT …)like Gecko
Accept 表示浏览器能接收的资源类型,如text/*,image/或者/*表示所有;
Accept-Language 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
Accept-Encoding 表示浏览器可以支持的压缩类型,例如gzip, deflate等。
Content-Type 请求主体的数据类型
Content-Length 数据主体的大小(单位:字节)
  • 请求体:存储请求参数(但是这里的GET方式请求参数在请求行中,不需要设置请求体)

POST方式


请求行:包含请求方式、资源路径、协议版本
请求头:格式key:value
请求体:存储请求参数,请求体于请求头之间是有一行空格隔开(便于标记)

区别方式 GET请求 POST请求
请求参数 请求参数在请求行中。
例:/brand/findAll?name=OPPO&status=1
请求参数在请求体中
请求参数长度 请求参数长度有限制(浏览器不同限制也不同) 请求参数长度没有限制
安全性 安全性低。原因:请求参数暴露在浏览器地址栏中。 安全性相对高

获取请求数据

HTTP响应协议

响应协议:服务器将数据以响应格式返回给浏览器。包括:相应行、响应头、响应体

  • 200 ok 客户端请求成功
  • 404 Not Found 请求资源不存在
  • 500 Internal Server Error 服务端发生不可预期的错误

响应数据

Web服务器对http协议的响应数据进行了封装(HttpServletResponse),并在调用Controller方法时传递了该方法。类似的,在请求协议里也进行了封装(HttpServletRequest)

例子

希望实现:

当在浏览器中访问前端静态页面 localhost:8080/user.html后,在前端页面上回发送ajax请求,请求服务端 localhost:8080/list ,服务器端程序加载user.txt文件中的数据,读取出来后最终给前端页面响应json格式的数据,前端页面再将数据渲染展示在表格中

总结

本案例要展示的核心功能:

  • 前端展示:希望通过静态html页面,展示用户数据表格
  • 后端服务:
    • 读取user.txt中的数据
    • 将数据解析为User对象集合
    • 通过RESTful接口返回json格式数据

涉及到的知识点:
SpringBoot

  • @RestController注解:组合了@Controller和@ResponseBody,直接返回JSON数据
  • 在使用 @RestController 注解标记的类中,每个方法的返回值都会以 JSON 或 XML 的形式直接写入 HTTP 响应体中,相当于在每个方法上都添加了 @ResponseBody 注解。

文件操作:

  • 资源文件加载:ClassLoader.getResourceAsStream()
  • Hutool工具库的IoUtil.readLines()高效读取文件
  • 文件路径:src/main/resources下的文件会被打包到classpath

三层架构思想

  • Controller:接收请求和响应数据
  • Service:业务逻辑处理(虽然本示例未显式分层,但隐含该思想)
  • DAO:数据访问(本示例中直接操作文件)

我们定义了一个实体类User:

1
2
3
4
5
6
7
8
@Data // Lombok自动生成getter/setter
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class User {
private Integer id;
private String username;
// 其他字段...
}

这里使用了Lombok自动生成getter和setter
Lombok简介、使用、工作原理、优缺点-CSDN博客

1
2
3
4
5
6
7
8
9
@Setter 注解在类或字段,注解在类时为所有字段生成setter方法,注解在字段上时只为该字段生成setter方法。
@Getter 使用方法同上,区别在于生成的是getter方法。
@ToString 注解在类,添加toString方法。
@EqualsAndHashCode 注解在类,生成hashCode和equals方法。
@NoArgsConstructor 注解在类,生成无参的构造方法。
@RequiredArgsConstructor 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。
@AllArgsConstructor 注解在类,生成包含类中所有字段的构造方法。
@Data 注解在类,生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
@Slf4j 注解在类,生成log变量,严格意义来说是常量。private static final Logger log = LoggerFactory.getLogger(UserController.class);

控制器:(这里给页面响应的代码全部堆积了,实际开发时我们会采用分层解耦的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController // = @Controller + @ResponseBody
public class UserController {

@RequestMapping("/list") // 映射请求路径
public String list() {
// 1. 文件读取
InputStream in = getClass().getResourceAsStream("user.txt");

// 2. 数据转换
List<User> users = lines.stream().map(line -> {
// 字符串解析逻辑...
return new User(id, username,...);
}).collect(Collectors.toList());

// 3. 返回JSON
return JSONUtil.toJsonStr(users);
}
}

这里用到了RESTful接口设计

前端交互部分:

1
2
3
4
5
fetch("/list")
.then(res => res.json())
.then(data => {
// 动态渲染表格
});

前端通过AJAX消费后端API

分层解耦

三层架构

  • 在进行程序设计及开发时,尽可能让每天接口、类、方法的职责更单一(单一职责)

前面入门程序:

但现在,我们应该把它拆开

  • 数据访问:负责业务数据的维护操作(增删改查)
  • 逻辑处理:负责业务的逻辑代码
  • 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

高内聚低耦合

  • 内聚: 软件中各个功能模块内部的功能联系。
  • 耦合: 衡量软件中各个层/模块之间的依赖、关联的程度。

解耦思路

不要在层里new 对象,很容易导致层与层之间代码耦合。
=>将要用的对象交给一个容器管理
=>应用程序中要到这个对象就从容器中获取

设计到两个概念:控制反转和依赖注入

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
    • 对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器。
  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
    • 程序运行时需要某个资源,此时容器就为其提供这个资源。
    • 例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象。
  • bean对象: IOC容器中创建、管理的对象,称之为:bean对象。

IOC和DI

1). 将Service及Dao层的实现类,交给IOC容器管理
在实现类加上 @Component 注解,就代表把当前类产生的对象交给IOC容器管理。
2). 为Controller 及 Service注入运行时所依赖的对象
@Autowired // 应用程序运行时,会自动查询该类型的bean对象,并赋值给该成员变量

说明 位置 注解
声明bean的基础注解 不属于以下三类时,用此注解 @Component
@Component的衍生注解 标注在控制层类上 @Controller
@Component的衍生注解 标注在业务层类上 @Service
@Component的衍生注解 标注在数据访问层类上(由于与mybatis整合,用的少) @Repository

[!NOTE]
注意1:声明bean的时候,可以通过注解的value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
注意2:使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
Bean对象:由Spring IOC容器创建、组装和管理的Java对象(也就是被Spring托管的对象)

  • 生命周期:

    创建Bean的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. Service层Bean
@Service
public class UserServiceImpl implements UserService {
// Spring会自动创建这个类的实例并管理
}

// 2. Controller层Bean
@RestController // = @Controller + @ResponseBody
public class UserController {
// Spring会自动创建并管理
}

// 3. Dao层Bean
@Repository
public class UserDaoImpl implements UserDao {
// Spring会自动创建并管理
}

// 4. 普通组件Bean
@Component
public class SomeUtil {
// 通用组件
}

Bean的依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

@Autowired // Spring自动注入Bean
private UserService userService; // 这里注入的是Bean对象

// 使用注入的Bean
@RequestMapping("/list")
public List<User> list() {
return userService.findAll(); // 调用Bean的方法
}
}

从bean的生命周期图中看到,为了让bean生效,还需要让它被组件扫描@ComponentScan

  • 该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包。

DI

依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

@Autowired用法

属性注入

1
2
3
4
5
6
7
8
@RestController
public class UserController {

//方式一: 属性注入
@Autowired
private UserService userService;

}

一个类依赖了哪些外部组件,通过属性注入是无法一眼看出来的。你必须阅读整个类的所有字段才能知道。(缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性。)

构造函数注入

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {

//方式二: 构造器注入
private final UserService userService;

@Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略
public UserController(UserService userService) {
this.userService = userService;
}

}
  • 优点:能清晰地看到类的依赖关系、提高了代码的安全性。
  • 缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿。

setter注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 用户信息Controller
*/
@RestController
public class UserController {

//方式三: setter注入
private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}

}
  • 优点:保持了类的封装性,依赖关系更清晰。
  • 缺点:需要额外编写setter方法,增加了代码量。

如果我们定义了多个相同的类,容器里会存在多个相同类型的bean,而框架不知道具体要注入哪个bean使用,会报错。

  • @Primary(存在相同的bean注入时,有这个注解的是默认的实现)
  • @Qualifier(指定当前要注入的bean对象–指定注入的bean名称)
    • @Qualifier注解不能单独使用,必须配合@Autowired使用。
  • @Resource(是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。)

@Autowired和@Resource区别:

  • 前者是spring框架提供的注解,而后者是JDK提供的注解
  • @Autowired默认是按类型注入,而后者是按照名称注入