Web基础 -SpringBoot入门 -HTTP-分层解耦 -三层架构
在HTML、CSS、JS 以及图片、音频、视频等这些资源,我们都称为静态资源。 所谓静态资源,就是指在服务器上存储的不会改变的数据,通常不会根据用户的请求而变化。
那与静态资源对应的还有一类资源,就是动态资源。那所谓动态资源,就是指在服务器端上存储的,会根据用户请求和其他数据动态生成的,内容可能会在每次请求时都发生变化。 在企业开发项目中,都是基于Spring实现的。
而Spring家族旗下这么多的技术,最基础、最核心的是 SpringFramework。其他的spring家族的技术,都是基于SpringFramework的,SpringFramework中提供很多实用功能,如:依赖注入、事务管理、web开发支持、数据访问、消息服务等等。
而如果我们在项目中,直接基于SpringFramework进行开发,存在两个问题:
配置繁琐
入门难度大
所以基于此呢,spring官方推荐我们从另外一个项目开始学习,那就是目前最火爆的SpringBoot。 通过springboot就可以快速的帮我们构建应用程序,所以springboot呢,最大的特点有两个 :
-
简化配置
-
快速开发
Spring Boot 可以帮助我们非常快速的构建应用程序、简化开发、提高效率 。
而直接基于SpringBoot进行项目构建和开发,不仅是Spring官方推荐的方式,也是现在企业开发的主流。
一、创建SpringBoot项目
左侧生成器选择SpringBoot,右侧选择Maven依赖即可。
下一步后勾选SpringWeb,项目即创建完成。
基础项目入门
package org.example.springbootwebquickstract;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){System.out.println("name:"+name);return "hello:"+name;}
}
上述代码中,注解@RestController起到标注当前的类为请求处理类的作用,注解@RequestMapping起到标注请求路径的作用。括号内的hello即为请求路径。
当需要访问对应数据时,可以用请求路径访问,例如本程序可以使用http://localhost:8080/hello?name=CodeBlossom进行访问
运行程序
运行SpringBoot自动生成的引导类 (标识有@SpringBootApplication注解的类)即可自动运行引导类下的包及其子包下的程序。
HTTP协议
HTTP:Hyper Text Transfer Protocol(超文本传输协议),规定了浏览器与服务器之间数据传输的规则。
-
http是互联网上应用最为广泛的一种网络协议
-
http协议要求:浏览器在向服务器发送请求数据时,或是服务器在向浏览器发送响应数据时,都必须按照固定的格式进行数据传输
HTTP协议的特点
基于TCP:面向连接,安全
基于请求-响应模型:一次请求对应一次响应,先请求后响应。请求和响应是一一对应的关系,没有请求就没有响应。
HTTP 协议分为:请求协议和响应协议
请求协议:浏览器将数据以请求格式发送到服务器。包括:请求行、请求头 、请求体
常见的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 | 数据主体的大小(单位:字节) |
获取请求体数据
Web对HTTP协议的请求数据进行解析,并进行了封装(HttpServletRequest),使程序员可以直接用代码获取请求体数据
package org.example.springbootwebquickstract;import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RequestController {@RequestMapping("/request")public String resquest(HttpServletRequest request){//1、获取请求方式String method = request.getMethod();System.out.println("method:"+method);//2、获取请求url地址String url = request.getRequestURL().toString();//http://localhost:8080/requestSystem.out.println("请求url地址:"+url);String requestURI = request.getRequestURI();System.out.println("请求uri地址:"+requestURI);//3、获取请求协议String protocol = request.getProtocol();System.out.println("请求协议"+protocol);//4、获取请求参数 -name,age .....String name = request.getParameter("name");String age = request.getParameter("age");System.out.println("name:"+name+",age:"+age);//5、获取请求头:AcceptString header = request.getHeader("Accept");//指定名字的请求头System.out.println("请求头Accept:"+header);return "OK";}
}
HTTP响应协议
响应体的第一行为响应行 分为 协议版本:HTTP/1.1 响应状态码401 以及状态码描述
响应头:响应数据的第二行开始。格式为key:value形式
http是个无状态的协议,所以可以在请求头和响应头中设置一些信息和想要执行的动作,这样,对方在收到信息后,就可以知道你是谁,你想干什么
常见的HTTP响应头有:
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg ; Content-Length:表示该响应内容的长度(字节数);
Content-Encoding:表示该响应压缩算法,例如gzip ; Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ;
Set-Cookie: 告诉浏览器为当前页面所在的域设置cookie ;
响应体(以上图中绿色部分): 响应数据的最后一部分。存储响应的数据
代码示例
package org.example.springbootwebquickstract;import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class ResponseController {/*方式一:设置响应数据基于HttpServletResponse对象*/@RequestMapping("/response")public void response(HttpServletResponse response) throws Exception{//1、设置响应状态码response.setStatus(HttpServletResponse.SC_OK);//2、设置响应头response.setHeader("Name","CodeBlossom");//3、设置响应体response.getWriter().write("<h1>Hello World</h1>");}/*方式二:设置响应数据基于ResponseEntity对象(spring提供的方式)*/@RequestMapping("/response2")public ResponseEntity< String> response2(){return ResponseEntity.status(401).header("name","javaweb").body("<h1>Hello response</h1>");}
}
SpringBootWeb案例
基于SpringBoot开发web程序,实现页面的渲染展示
先创建一个封装用户信息的实体类
package org.example.springbootweb01.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {private Integer id;//项目开发建议使用包装类型private String username;private String password;private String name;private Integer age;private LocalDateTime updateTime;}
由于在案例中,需要读取文本中的数据,并且还需要将对象转为json格式,所以这里呢,我们在项目中再引入一个非常常用的工具包hutool。 然后调用里面的工具类,就可以非常方便快捷的完成业务操作。
在Maven带有的xml文件中添加依赖
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.27</version>
</dependency>
package org.example.springbootweb01.controller;import cn.hutool.core.io.IoUtil;
import jakarta.annotation.Resource;
import org.example.springbootweb01.pojo.User;
import org.example.springbootweb01.service.UserService;
import org.example.springbootweb01.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;/*
用户信息Controller*/
@RestController//@ResponseBody ->作用:将controller返回值直接作为响应体的数据直接响应;返回值是对象/集合->json->响应体
public class UserController {@RequestMapping("/list")public List<User> list()throws Exception{//InputStream in = new FileInputStream(new File("src/main/resources/user.txt")); //磁盘目录会改变,所以不推荐InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt"); //类加载器,将文件放在类路径下,通过类加载器获取//1、加载并读取User.txt文件获取用户数据ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8,new ArrayList<>());//2、解析用户信息,将用户数据封装成User对象->List集合List<User> userList = 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();//3、返回用户数据(json)return userList;}}
将前端代码放到Spring标注的静态资源库中
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>用户列表数据</title><style>/*定义css,美化表格*/table{border-collapse: collapse;width: 100%;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;font-family: 楷体;}</style>
</head>
<body><div id="app"><h1>用户列表数据</h1><!--定义一个表格,包括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 in userList"><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="js/axios.min.js"></script><script type="module">import { createApp } from './js/vue.esm-browser.js'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>
@ResponseBody(包含在@RestController内)注解:
-
类型:方法注解、类注解
-
位置:书写在Controller方法上或类上
-
作用:将方法返回值直接响应给浏览器,如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器
-
三层架构
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。这样可以让代码维护更简单
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
逻辑处理:负责业务逻辑处理的代码。
请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在项目开发中,可以将代码分为三层,如图所示:
-
Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
-
Service:业务逻辑层。处理具体的业务逻辑。
-
Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
执行流程
浏览器访问页面->控制层->业务逻辑层->数据访问层,这样实现了分层解耦思想,例如对业务层进行变更,不会影响到控制层和Dao层。
分层解耦的思想
耦合:衡量软件中各个层/各个模块的依赖关联层度。
内聚:软件中各个功能模块的功能联系
软件设计原则:高内聚低耦合
上述的思想无法实现解耦思想
例如上图,在控制层需要指定new一个Uservice的实现类创建,当我们需要修改其使用的实现类时还是需要更改控制层。我们可以根据之前的内容实现这个问题。当我们定义常量时,可以专门创建一个常量的容器(类),然后当需要更改常量时在容器内将其更改即可,对调用的代码无影响。此时的解耦也可以用这个操作,将UserService的实现类放到容器中,然后将需要使用的实现类注入到对应的程序中。
们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:
-
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
-
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器。
-
-
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
-
程序运行时需要某个资源,此时容器就为其提供这个资源。
-
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象。
-
Bean的声明
-
bean对象:IOC容器中创建、管理的对象,称之为:bean对象。
实现类加上 @Component
注解,就代表把当前类产生的对象交给IOC容器管理。
在需要注入容器中的类的对象加上@Autowired注解,就代表把容器内的实现类交给当前的对象。
前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
注解 | 说明 | 位置 |
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制层类上 |
@Service | @Component的衍生注解 | 标注在业务层类上 |
@Repository | @Component的衍生注解 | 标注在数据访问层类上(由于与mybatis整合,用的少) |
DI详解
依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。
@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)
@Autowired用法
属性注入(常用)
@RestController
public class UserController {//方式一: 属性注入@Autowiredprivate UserService userService;}
-
优点:代码简洁、方便快速开发。
-
缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性。
构造函数注入(常用)
@RestController
public class UserController {//方式二: 构造器注入private final UserService userService;@Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略public UserController(UserService userService) {this.userService = userService;}}
-
优点:能清晰地看到类的依赖关系、提高了代码的安全性。
-
缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿。
-
注意:如果只有一个构造函数,@Autowired注解可以省略。(通常来说,也只有一个构造函数)
3). setter注入
/*** 用户信息Controller*/
@RestController
public class UserController {//方式三: setter注入private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}}
优点:保持了类的封装性,依赖关系更清晰。
缺点:需要额外编写setter方法,增加了代码量。
注意事项:
在IOC容器中,存在多个相同类型的bean对象,容器会不知道该给当前的对象注入哪个bean对象,程序会报错。
为了解决上述问题,Spring给了以下三种方案
-
@Primary
-
@Qualifier
-
@Resource
方案一:使用@Primary注解
当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
在想要注入的bean对象上加此注解即可默认注入该个对象
方案二:使用@Qualifier注解(bean对象的默认名为实现类的首字母小写)
指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。 @Qualifier注解不能单独使用,必须配合@Autowired使用。例:@Qualifier("bean对象")
方案三:使用@Resource注解
是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。例:
@Resource(name = "userServiceImpl")
项目构建
package org.example.springbootweb01.dao;import java.util.List;public interface UserDao {public List<String> findAll();
}
package org.example.springbootweb01.service;import org.example.springbootweb01.pojo.User;import java.util.List;public interface UserService {public List<User> findAll();
}
package org.example.springbootweb01.controller;import cn.hutool.core.io.IoUtil;
import jakarta.annotation.Resource;
import org.example.springbootweb01.pojo.User;
import org.example.springbootweb01.service.UserService;
import org.example.springbootweb01.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;/*
用户信息Controller*/
@RestController//@ResponseBody ->作用:将controller返回值直接作为响应体的数据直接响应;返回值是对象/集合->json->响应体
public class UserController {
// 1部分/*@RequestMapping("/list")public List<User> list()throws Exception{//InputStream in = new FileInputStream(new File("src/main/resources/user.txt")); //磁盘目录会改变,所以不推荐InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt"); //类加载器,将文件放在类路径下,通过类加载器获取//1、加载并读取User.txt文件获取用户数据ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8,new ArrayList<>());//2、解析用户信息,将用户数据封装成User对象->List集合List<User> userList = 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();//3、返回用户数据(json)return userList;}*/
//2部分/*private UserService userService = new UserServiceImpl();@RequestMapping("/list")public List<User> list()throws Exception{List<User> userList =userService.findAll();//3、返回用户数据(json)return userList;*///方式一:属性注入//@Autowired //@Autowired注解:自动注入//private UserService userService;
// //方式二:构造方法注入
// private final UserService userService;
// @Autowired //如果构造函数只有一个,@Autowired可以省略
// public UserController(UserService userService) {
// this.userService = userService;
// }//方式三:setter方法注入private UserService userService;//@Qualifier("userServiceImpl")//@Autowired@Resource(name = "userServiceImpl")public void setUserService(UserService userService) {this.userService = userService;}@RequestMapping("/list")public List<User> list()throws Exception{List<User> userList =userService.findAll();//3、返回用户数据(json)return userList;}
}
package org.example.springbootweb01.dao.impl;import cn.hutool.core.io.IoUtil;
import org.example.springbootweb01.dao.UserDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Repository("userDao")//括号内指定bean的名字,默认为类名首字母小写
//@Component //将当前类交给ioc容器管理
public class UserDaoImpl implements UserDao {@Overridepublic List<String> findAll() {//InputStream in = new FileInputStream(new File("src/main/resources/user.txt"));//1、加载并读取User.txt文件获取用户数据InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8,new ArrayList<>());return lines;}
}
package org.example.springbootweb01.service.impl;import org.example.springbootweb01.dao.UserDao;
import org.example.springbootweb01.dao.impl.UserDaoImpl;
import org.example.springbootweb01.pojo.User;
import org.example.springbootweb01.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
//@Component//将当前类交给ioc容器管理
public class UserServiceImpl implements UserService {//1、调用dao获取数据//private UserDao userDao = new UserDaoImpl();@Autowired //应用程序运行时会自动查找该类型的Bean对象,并赋值给该成员变量private UserDao userDao;@Overridepublic List<User> findAll() {List<String> lines = userDao.findAll();//2、解析用户信息,将用户数据封装成User对象->List集合List<User> userList = 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();return userList;}
}
package org.example.springbootweb01.service.impl;import org.example.springbootweb01.dao.UserDao;
import org.example.springbootweb01.pojo.User;
import org.example.springbootweb01.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
//@Primary // 指定当前类为默认实现类
@Service
//@Component//将当前类交给ioc容器管理
public class UserServiceImpl2 implements UserService {//1、调用dao获取数据//private UserDao userDao = new UserDaoImpl();@Autowired //应用程序运行时会自动查找该类型的Bean对象,并赋值给该成员变量private UserDao userDao;@Overridepublic List<User> findAll() {List<String> lines = userDao.findAll();//2、解析用户信息,将用户数据封装成User对象->List集合List<User> userList = 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+200, username, password, name, age, updateTime);}).toList();return userList;}
}
上述代码实现了分层解耦思想。