目录
SpringBootWeb请求响应
前言
1. 请求
1.1 Postman
1.2 简单参数
1.3 实体参数
1.4 数组集合参数
1.5 日期参数
1.6 JSON参数
1.7 路径参数
2. 响应
2.1 介绍
2.2 @ResponseBody
2.3 统一响应结果
2.4 案例
3. 分层解耦
3.1 三层架构
3.2 分层解耦
3.3 IOC&DI
在上一次的课程中,我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用,浏览器发起请求 /hello 后 ,给浏览器返回字符串 “Hello World ~”。
其实呢,是我们在浏览器发起请求,请求了我们的后端web服务器,也就是内置的Tomcat。而我们在开发web程序时呢,定义了一个控制器类Controller,请求会被部署在Tomcat中的Controller接收,然后Controller再给浏览器一个响应,响应一个字符串 “Hello World”。 而在请求响应的过程中是遵循HTTP协议的。
但是呢,这里要告诉大家的时,其实在Tomcat这类Web服务器中,是不识别我们自己定义的Controller的。但是我们前面讲到过Tomcat是一个Servlet容器,是支持Serlvet规范的,所以呢,在tomcat中是可以识别 Servlet程序的。 那我们所编写的XxxController 是如何处理请求的,又与Servlet之间有什么联系呢?
其实呢,在SpringBoot进行web程序开发时,它内置了一个核心的Servlet程序 DispatcherServlet,称之为 核心控制器。 DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器Controller,请求处理器处理完请求之后,最终再由DispatcherServlet给浏览器响应数据。
那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。
可以自己实验,体验一下原生Serverlet开发:
实验步骤:
1、创建类HelloServerlet继承 HttpServerlet,重写service方法
2、在启动类上加上一个注解:@ServletComponentScan
package com.itheima.springbootquikstart;import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author HuanLe* @version 1.0*/
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {@Overridepublic void service(HttpServletRequest request, HttpServletResponse response) throws IOException {String username = request.getParameter("username");String age = request.getParameter("age");System.out.println("收到数据:" + username + " - " + age);response.setHeader("content-type", "text/html;charset=utf8");response.getWriter().println("hello Servlet哈哈!!");response.getWriter().println("响应接收到的数据: " + username + " " + age);}
}
执行后到效果:
那上述所描述的这种浏览器/服务器的架构模式呢,我们称之为:BS架构。
• BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
那今天呢,我们的课程内容主要就围绕着:请求、响应进行。 今天课程内容,主要包含三个部分:
在本章节呢,我们主要讲解,如何接收页面传递过来的请求数据。
1.1.1 介绍
1.1.2 安装
百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固,支持教育网加速,支持手机端。注册使用百度网盘即可享受免费存储空间
界面介绍:
如果我们需要将测试的请求信息保存下来,就需要创建一个postman的账号,然后登录之后才可以。
1.2.1 原始方式
通过Servlet中提供的API HttpServletRequest 可以获取请求的相关信息,比如获取请求参数:
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){String name = request.getParameter("name");String age = request.getParameter("age");System.out.println(name+" : "+age);return "OK";
}
在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明 HttpServletRequest 对象。然后就可以通过该对象来获取请求信息:
获取请求参数:request.getParameter("参数名")
postman测试:
1.2.2 SpringBoot方式
在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){System.out.println(name+" : "+age);return "OK";
}
postman测试( GET 请求):
postman测试( POST请求 ):
我们可以点击上面的菜单栏: View ----> Show Postman Console
1.2.3 参数名不一致
如果方法形参名称与请求参数名称不一致,可以使用 @RequestParam 完成映射。
如果请求参数名为 username,controller方法形参使用name进行接收,是不能够直接封装的,需要在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射,代码如下:
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam("username") String name , Integer age){System.out.println(name+" : "+age);return "OK";
}
如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。 此时,我们可以考虑将请求参数封装到一个 pojo 对象中。 此时,要想完成数据封装,需要遵守如下规则:请求参数名与POJO的属性名相同
1.3.1 简单实体对象
1). 定义POJO实体类
public class User {private String name;private Integer age;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
lombom使用
在pom清单加依赖
//
//
//
//
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/*** @author HuanLe* @version 1.0*/
@Data@NoArgsConstructor//无参构造@AllArgsConstructor//满参构造器@EqualsAndHashCode//hashCode和equalspublic class User {private String name;private Integer age;// 为什么不用int?使用Integer的原因是,如果前端没有穿数据默认是null。如果使用int默认为0.//构造器//getter/setter//toString//让lombok去完成}
2). controller 方法
@RequestMapping("/simplePojo")
public String simplePojo(User user){System.out.println(user);return "OK";
}
3). postman
1.3.2 复杂实体对象
复杂实体对象指的是,在实体类中有一个或多个属性,也是实体类型的。如下:
public class Address {private String province;private String city;public String getProvince() {return province;}public void setProvince(String province) {this.province = province;}public String getCity() {return city;}public void setCity(String city) {this.city = city;}@Overridepublic String toString() {return "Address{" +"province='" + province + '\'' +", city='" + city + '\'' +'}';}
}
public class User {private String name;private Integer age;private Address address;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public Address getAddress() {return address;}public void setAddress(Address address) {this.address = address;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +", address=" + address +'}';}
}
复杂实体对象的封装,需要遵守如下规则:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数。
1). Controller 方法
@RequestMapping("/complexPojo")
public String complexPojo(User user){System.out.println(user);return "OK";
}
2). Postman
1.4.1 数组
1). Controller方法
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){System.out.println(Arrays.toString(hobby));return "OK";
}
2). Postman
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx? hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
1.4.2 集合
1). Controller方法
@RequestMapping("/listParam")
public String listParam(@RequestParam List
}
2). Postman
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx? hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:
那对于日期类型的参数在进行封装的时候,需要通过 @DateTimeFormat注解,以及其中的pattern属性来设置日期的格式。因为日期的格式多种多样,请求pattern属性中如何制定,前端传递参数时就怎么传递。
1). Controller方法
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){System.out.println(updateTime);return "OK";
}
2). Postman
其实呢,在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 而传递json格式的参数,服务端,在Controller中,我们通常会使用实体类进行封装。 具体的封装规则如下:
JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody 标识。
1). 实体类
public class Address {private String province;private String city;//省略GET , SET 方法
}public class User {private String name;private Integer age;private Address address;//省略GET , SET 方法
}
2). Controller方法
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){System.out.println(user);return "OK";
}
3). Postman
注意:在测试时,要使用post请求,并且到body中写JSON字符串
如下:
处理上述演示的在请求体传递参数,以及在请求的url后面通过 ?xxx=xxx 的形式传递参数以外,在现在的开发中,经常还会直接在请求的URL中传递参数。比如:
http://localhost:8080/user/1
http://localhost:880/user/1/0
上述的这种传递请求参数的形式呢,我们称之为:路径参数。
1). Controller方法
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){System.out.println(id);return "OK";
}@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){System.out.println(id+ " : " +name);return "OK";
}
2). Postman
在上述的方法中,我们在测试的时候,统一给页面响应了一个简单的字符串 "OK"。 其实,我们也可以直接将一个实体对象,或者一个集合直接响应回去。
比如如下这样:
A. 响应字符串 OK
@RequestMapping("/simpleParam")
public String simpleParam( String name , Integer age){System.out.println(name+" : "+age);return "OK";
}
B. 响应实体对象
@RequestMapping("/getUser")
public User getUser(){User user = new User();user.setName("Tom");user.setAge(10);return user;
}
C. 返回集合数据
@RequestMapping("/list")
public List
}
那在服务端,我们直接响应了一个对象 或者 集合,那最终前端获取到的数据是什么样子的呢?我们可以测试一下,通过postman发送请求,测试效果如下:
我们响应的是一个java对象 或者 集合, 怎么最终返回的确实JSON格式的数据呢 ? 其实啊,这是 @ResponseBody 注解的作用。
而我们的案例中,并没有直接使用 @ResponseBody,原因是因为我们使用的是 @RestController注解,该注解中已经封装了@ResponseBody注解,已经包含了@ResponseBody注解的作用,我们无需要额外添加。
但是呢,我们发现,我们在上述所编写的这些个Controller的方法,返回值各种各样,没有任何的规范。如果我们开发一个大型项目也是这样,那整个项目将难以维护。那在真实的项目开发中是什么样子的呢?
在真实的项目开发中,无论是增删改查的那种方法,我们都会定义一个统一的返回结果,在这个返回结果中,包含一下信息:
A. 当前请求是成功,还是失败。
B. 当前给页面的提示信息。
C . 返回的数据。
对于上述的这些数据呢,我们一般都会定义在一个实体类Result中。 代码如下:
public class Result {private Integer code;//响应码,1 代表成功; 0 代表失败private String msg; //响应码 描述字符串private Object data; //返回的数据public Result() { }public Result(Integer code, String msg, Object data) {this.code = code;this.msg = msg;this.data = data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}//增删改 成功响应public static Result success(){return new Result(1,"success",null);}//查询 成功响应public static Result success(Object data){return new Result(1,"success",data);}//失败响应public static Result error(String msg){return new Result(0,msg,null);}
}
2.4.1 需求说明
获取用户数据,返回统一响应结果,在页面渲染展示
2.4.2 准备工作
在SpringBoot项目中,静态资源可以存放的目录:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
classpath:
代表的是类路径,在maven的项目中,其实指的就是 src/main/resources 或者 src/main/java,但是java目录是存放java代码的,所以相关的配置文件及静态资源文档,就放在 src/main/resources下。
2.4.3 代码实现
import com.itheima.springbootquikstart.pojo.Address;
import com.itheima.springbootquikstart.pojo.Result;
import com.itheima.springbootquikstart.pojo.User;
import com.itheima.springbootquikstart.util.XmlParserUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @author HuanLe* @version 1.0*/@RestController
public class UserController {@RequestMapping("/listUser")public Result listUser() {//1. 获取数据String file = ClassLoader.getSystemResource("user.xml").getFile();//调用工具类去解析List
2.4.4 测试
代码编写完毕之后,我们就可以运行引导类,启动服务进行测试了。 打开浏览器,在浏览器地址栏输入:
http://localhost:8080/user.html
2.4.5 问题分析
上述案例的功能,我们虽然已经实现,但是呢,我们会发现案例中:解析XML数据,获取数据的代码,处理数据的逻辑的代码,给页面响应的代码全部都堆积在一起了,全部都写在控制器Controller中了。 当然这个业务逻辑还是比较简单的,如果业务逻辑再稍微复杂一点,我们会看到Controller 方法的代码量就很大了,我们要修改操作数据部分的代码,需要改动Controller; 我们要完善逻辑处理部分的代码,我们需要改动Controller;我们需要修改数据响应的代码,我们还是需要改动Controller。
这样呢,就会造成我们整个工程代码的复用性比较差,而且代码难以维护。 那如何解决这个问题呢,其实在现在的开发中,有非常成熟的解决思路,那就是分层开发。
3.1.1 介绍
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
按照上述的三个组成部分,在我们的业务开发中呢,按照这三个部分,我们将代码分为三层:
3.1.2 代码拆分
1). UserController
接收前端发送的请求,对请求进行处理,并响应数据
import com.itheima.domain.Result;
import com.itheima.domain.User;
import com.itheima.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;/*** @author HuanLe* @version 1.0*/@RestController
public class UserController {private UserServiceA userService = new UserServiceA();@RequestMapping("/listUser")public Result listUser() throws Exception {// 调用service, 获取数据List
2). UserServiceA
处理具体的业务逻辑
import com.itheima.dao.UserDao;
import com.itheima.domain.Address;
import com.itheima.domain.User;
import java.util.List;/*** @author HuanLe* @version 1.0*/public class UserServiceA {private UserDaoA userDao = new UserDaoA();public List
3). UserDaoA
负责数据的访问操作,包含数据的增、删、改、查
import com.itheima.controller.UserController;
import com.itheima.domain.User;
import com.itheima.utils.XmlParserUtils;
import java.util.List;/*** @author HuanLe* @version 1.0*/public class UserDaoA {public List
3.2.1 耦合问题
那这里呢,我们首先需要了解软件开发领导涉及到的两个概念:内聚和耦合。
1).内聚:软件中各个功能模块内部的功能联系。
2).耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
3).软件设计原则:高内聚低耦合。
像我们前面开发的加载XML数据,展示用户信息列表的案例中。我们基于三层架构对原始的代码进行了改造,分为了controller、service以及dao三层。在拆分的这三层中 controller调用service, service调用dao 。而在controller中调用service,我们就需要在controller中new一个service对象。 而在service中需要调用dao,我们就在service中new一个dao对象。 那么此时 controller的代码就耦合了service , 而service的代码也就耦合了dao。
而在软件开发领域,我们经常会提到一种设计原则:高内聚,低耦合。 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。低耦合指的是:软件中各个层、模块之间的依赖关联程度越低越好。
高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。
3.2.2 解耦思路
而之前我们的代码,在编写的时候,需要什么对象,就直接new一个就可以了。 这样呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。
此时我们来看,如果service层的实现一旦发生变化,比如: 我们要使用UserServiceB,不使用原来的UserServiceA了。 那么此时,我们还需要修改UserController层的代码。 需要将Controller中变量声明,以及创建实例的代码都改动。
那此时,Controller与Service层的代码是耦合的。
那为了解耦呢,我们需要为UserServiceA 与 UserServiceB 这两个不同的实现类,定义一个统一的接口,前端定义变量的时候,我们直接使用接口来声明就可以(多态的体现)。
具体代码为:
public interface UserService {public List
}
public class UserServiceA implements UserService {private UserDao userDao = new UserDaoA();public List
}
public class UserServiceA implements UserService {private UserDao userDao = new UserDaoA();public List
}
dao 层,为了更好的实现解耦,我们也可以为dao层提供一个接口 UserDao , 然后让实现类 UserDaoA 实现 UserDao。
首先,要想解耦,我们就不要在UserController中,直接去new service层的对象了,一旦我们new了,就耦合起来了。 那如果此时service层的实现类换了,换成UserServiceB了,我们就需要跟着修改UserController的代码。
如果我们直接删除掉后面的new对象的操作,此时如果程序直接运行,则会报错,报出空指针异常。
我们可以这样,可以将创建的实例对象,都存储在一个容器中,如果那么需要这个对象,就不用再去单独new对象了,直接在运行时通过容器提供就可以了。 比如:
我们可以将UserServiceA对象交给容器管理,UserController运行时需要userService,那么容器就给UserController提供这个userService对象。 此时如果UserService的实现类换了, 换成UserServiceB了, 我们只需要将UserServiceB产生的对象交给容器管理即可。
那这里呢,就涉及到两个过程:
1). 将对象交给容器管理的过程 , 称之为 控制反转。 Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。 而这个容器, 称之为IOC容器,或者Spring容器。
2). 应用程序运行时, 容器为其提供运行时所需要的资源, 这个过程我们称之为依赖注入。 Dependency Injection,简称DI。
3). IOC容器中创建、管理的对象,称之为bean。
3.3.1 IOC&DI入门
在类上加上 @Component 注解,就是将该类声明为IOC容器中的bean。
@Component
public class UserServiceA implements UserService {@Autowiredprivate UserDao userDao ;public List
}
@Component
public class UserDaoA implements UserDao {public List
}
在成员变量上加上 @Autowired 注解,表示在程序运行时,Springboot会自动的从IOC容器中找到UserService类型的bean对象,然后赋值给该变量。
@RestController
public class UserController {@Autowiredprivate UserService userService ;@RequestMapping("/listUser")public Result listUser() {List
}
运行引导类,启动完成之后,打开浏览器,输入:http://localhost:8080/user.html 访问
3.3.2 bean的声明
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合,用的少) |
注意事项:
3.3.3 组件扫描
1). 前面声明bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描。
2). @ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,默认扫描的范围是引导类所在包及其子包。
标准的目录结构,如下:
将我们定义的controller,service,dao这些包呢,都放在引导类所在包 com.itheima 的子包下,这样我们定义的bean就会被自动的扫描到。
3.3.4 依赖注入
使用@Autowired 注解,表示在程序运行时,Springboot会自动的从IOC容器中找到UserService类型的bean对象,然后赋值给该变量。
但是需要注意:@Autowired注解,默认是按照类型进行,如果存在多个相同类型的bean,将会报出如下错误:
我们可以通过如下几种方案来解决:
1). @Primary 注解
当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
2). @Qualifier 注解
可以通过@Autowired ,配合@Qualifier 来指定我们当前要注入哪一个bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
3). @Resource注解
通过@Resource注解,并指定其name属性,通过name指定要注入的bean的名称。这种方式呢,是按照bean的名称进行注入。
@Autowird 与 @Resource的区别: