day05-SpringBootWeb请求响应学习笔记

骨Zi里的傲慢欢hhh 2024-08-25 10:33:01 阅读 74

上面说过,浏览器向服务端发送请求,服务端会给浏览器发送出响应,无论是哪种,都包含三部分。这一章,依旧围绕这部分内容

请求

Postman

由于前后端分离,对我们后端技术人员来讲,在开发过程中,是没有前端页面的,那我们怎么测试自己所开发的程序呢?

方式1:像之前SpringBoot入门案例中一样,直接使用浏览器。在浏览器中输入地址,测试后端程序。

弊端:在浏览器地址栏中输入地址这种方式都是GET请求,如何我们要用到POST请求怎么办呢?

要解决POST请求,需要程序员自己编写前端代码(比较麻烦)

方式2:使用专业的接口测试工具(课程中我们使用Postman工具)

前后端开发是分离的,当我们做好后端程序后,没有前端的页面,那么该怎么知道做的对不对呢、当然可以是自己动手,丰衣足食。但是很麻烦且耽误效率。这个时候,就需要一个工具。可以让我们知道自己写的程序是没有问题的。

Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。

Postman原是Chrome浏览器的插件,可以模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求

使用Postman还可以在发起请求时,携带一些请求参数、请求头等信息

作用:常用于进行接口测试

特征

简单实用美观大方

这些都是一个套话,总而言之,Postman是一个用于接口测试的工具

这个软件解压双击就会完成安装。好像不可以自定义安装路径

简单参数

简单参数:在向服务器发起请求时,向服务器传递的是一些普通的请求数据。

url中包含参数,类似于下面的

<code>http://localhost:8080/simpleParam?name=Tom&age=10

在上面的url中name=Tomage=10就是一种请求数据。这个数据单一,也就是普通数据。

现在要考虑在后端怎么接收这个数据。拿到Tomage

原始方式

在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:

Tomcat接收到http请求时:把请求的相关信息封装到HttpServletRequest对象中

在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明 HttpServletRequest 对象。然后就可以通过该对象来获取请求信息:

//根据指定的参数名获取请求参数的数据值

String request.getParameter("参数名")

// http://localhost:8080/simpleParam-test1?name=Tom&age=22

// 第1个请求参数: name=Tom 参数名:name,参数值:Tom

// 第2个请求参数: age=10 参数名:age , 参数值:10

@RequestMapping("/simpleParam-test1")

public String simpleParam1(HttpServletRequest request) { -- -->

// 获取请求参数

String name = request.getParameter("name");

String ageStr = request.getParameter("age");

// 类型转换

int age = Integer.parseInt(ageStr);

System.out.println(name + "," + age); // Tom,22

return "ok";

}

在这里有个注意1=:在request.getParameter("name")和url这的name=应该是保持一致的,如果不一致,不会怎么样,返回默认值,现在我把url改成http://localhost:8080/simpleParam-test1?n1ame=Tom&age=22进行测试,页面上返回ok了。但是请移步后端程序

System.out.println(name + "," + age); // null,22

因为我把name写成n1ame了,因此String变量的默认值是null。

SpringBoot方式

在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。

// 基于springboot的

// http://localhost:8080/simpleParam-springboot?name=Tom&age=22

@RequestMapping("/simpleParam-springboot")

public String simpleParam2(String name, Integer age) {

// 获取请求参数

System.out.println(name + "," + age); // Tom,22

return "ok";

}

这个比原始方式的代码少多了,其实他的逻辑和原生方式都是一样的。

url是用户输入的,用户的输入是正确还是错误,我们是无法干预的,但是用户输错了,也就会出现很多问题。

http://localhost:8080/simpleParam-springboot?name=zhangsan&aage=12

这个url很明显输入有问题。后端拿不到正确的数据,服务器也就不知道返回什么了。

解决方案:可以使用Spring提供的@RequestParam注解完成映射

在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。

@RequestMapping("/simpleParam-springboot")

public String simpleParam2(@RequestParam("name") String name, Integer age) {

// 获取请求参数

System.out.println(name + "," + age);

return "ok";

}

接下来,如果访问前面个错误的url。浏览器会抛出一个错误

{

"timestamp": "2024-08-15T11:15:19.085+00:00",

"status": 400,

"error": "Bad Request",

"path": "/simpleParam-springboot"

}

​ 状态码是400,请求参数有问题

@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错

@RequestMapping("/simpleParam-springboot")

public String simpleParam2(@RequestParam("name") String name, @RequestParam(value = "age", required = false) Integer age) {

// 获取请求参数

System.out.println(name + "," + age);

return "ok";

}

如果是上面的代码,name是必须传递的,而age可以不用传递。

http://localhost:8080/simpleParam-springboot?name=zhangsan

可以在浏览器中访问到。

实体参数

简单参数的接收有个弊端,如果前端发过来的请求参数特别的,那么就需要一个一个参数的接收。例如:

String name = request.getParameter("name");

String ageStr = request.getParameter("age");

String gender = request.getParameter("gender");

...

或者

@RequestMapping("/simpleParam-springboot")

public String simpleParam2(String name, Integer age, String gender ...) {

return "ok";

}

此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同

把单独的请求对象全部封装进一个实体类对象。

简单的实体参数

定义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;

}

@Override

public String toString() {

return "User{" +

"name='" + name + '\'' +code>

", age=" + age +

'}';

}

}

Controller方法:

@RestController

public class RequestController {

//实体参数:简单实体对象

@RequestMapping("/simplePojo")code>

public String simplePojo(User user){

System.out.println(user);

return "OK";

}

}

编写完这两段代码之后,就可以使用postman测试了

当请求参数一致时,

http://localhost:8080/simplePojo?name=zhangsan&age=22

浏览器会返回ok,控制台会接收参数

System.out.println(user); // User{name='zhangsan', age=22}code>

当请求参数不一致时。

http://localhost:8080/simplePojo?name=zhangsan&agew=22

浏览器还会返回ok,此时看控制台

System.out.println(user); // User{name='zhangsan', age=null}code>

复杂的实体对象

复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:

User类中有一个Address类型的属性(Address是一个实体类)

复杂实体对象的封装,需要遵守如下规则:

请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。

复杂实体对象就是实体类中的一个或多个属性还是一个实体类。

public class User { -- -->

private String name;

private Integer age;

private Address address; //地址对象

...

}

在User类中多了一个address属性,address属性的类型是Address,Address还是一个实体类。

定义POJO实体类:

Address实体类

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;

}

@Override

public String toString() {

return "Address{" +

"province='" + province + '\'' +code>

", city='" + city + '\'' +code>

'}';

}

}

User实体类

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;

}

@Override

public String toString() {

return "User{" +

"name='" + name + '\'' +code>

", age=" + age +

", address=" + address +

'}';

}

}

Controller方法:

@RestController

public class RequestController {

//实体参数:复杂实体对象

@RequestMapping("/complexPojo")code>

public String complexPojo(User user){ -- -->

System.out.println(user);

return "OK";

}

}

Postman测试:

http://localhost:8080/complexPojo?name=zhangsan&agcew=22&address.province=beijing&address.city=beijing

Java后端接收到的是

System.out.println(user); // User{name='zhangsan', age=null, address=Address{province='beijing', city='beijing'}}code>

需要注意url中的参数address.province=address.city=应该和Address类中的属性值保持一致。

数组集合参数

数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。

这个我没有接触到。这里主要说的是多选框。先把这个参数接收到再说其他的

数组参数

数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

@RestController

public class RequestController { -- -->

//数组集合参数

@RequestMapping("/arrayParam")

public String arrayParam(String[] hobby){

System.out.println(Arrays.toString(hobby));

return "OK";

}

}

然后测试这个接口,有两种参数传递方式

方式一:

http://localhost:8080/arrayParam?hobby=game&hobby=java&hobby=swim

System.out.println(Arrays.toString(hobby)); // [game, java, swim]

方式二:

http://localhost:8080/arrayParam?hobby=game,java,swim

System.out.println(Arrays.toString(hobby)); // [game, java, swim]

集合参数

集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系

默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系

@RestController

public class RequestController {

//数组集合参数

@RequestMapping("/listParam")

public String listParam(@RequestParam List<String> hobby){

System.out.println(hobby);

return "OK";

}

}

http://localhost:8080/listParam?hobby=game&hobby=java&hobby=swim

http://localhost:8080/listParam?hobby=game,java,swim

System.out.println(hobby); // [game, java, swim]

集合参数传递也是两种方式。和数组大致相同。

日期参数

上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:

因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。

在一些表单中,需要填写日期,那么这就是日期参数,现在学习后台怎么拿到这个参数。

@DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。

Controller方法:

@RestController

public class RequestController {

//日期时间参数

@RequestMapping("/dateParam")

public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){

System.out.println(updateTime);

return "OK";

}

}

Postman测试:

JSON数据

在学习前端技术时,我们有讲到过JSON,而在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)

前后端参数传递主要的还是json,所以这个是重点。

如何用Postman发送json数据?首先必须是post请求,json数据需要放在请求体中。Body => raw => 选择JSON

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

服务端Controller方法接收JSON格式数据:

传递json格式的参数,在Controller中会使用实体类进行封装。封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。

JSON格式数据传递必须封装在实体中,所以就用上面写的User类和Address类来举例

Controller方法:

<code>@RestController

public class RequestController { -- -->

//JSON参数

@RequestMapping("/jsonParam")

public String jsonParam(@RequestBody User user){

System.out.println(user);

return "OK";

}

}

Postman测试:

输入如下面json内容

{

"name": "zhangsan",

"age": 22,

"address":{

"province": "北京",

"city": "北京"

}

}

System.out.println(user); // User{name='zhangsan', age=22, address=Address{province='北京', city='北京'}}code>

路径参数

传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。

上面的演示都是属于传统的开发,而现在的开发还会在url中传递路径

http://localhost:8080/user/1

http://localhost:880/user/1/0

上面两个url中,/1/1/0就是路径参数,后端是需要拿到的

路径参数:

前端:通过请求URL直接传递参数后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数

Controller方法:

@RestController

public class RequestController { -- -->

//路径参数

@RequestMapping("/path/{id}")

public String pathParam(@PathVariable Integer id){

System.out.println(id);

return "OK";

}

}

Postman测试:

http://localhost:8080/pathParam/1

System.out.println(id); // 1

也可以传递多个路径

@RestController

public class RequestController {

//路径参数

@RequestMapping("/path/{id}/{name}")

public String pathParam2(@PathVariable Integer id, @PathVariable String name){

System.out.println(id+ " : " +name);

return "OK";

}

}

http://localhost:8080/pathParam/1/tom

System.out.println(id+ " : " +name); // 1,tom

上面就是一些常见的参数请求传递

有请求,就应该有响应。

响应

@ResponseBody

@RestController

public class HelloController {

@RequestMapping("/hello")

public String hello(){

System.out.println("Hello World ~");

return "Hello World ~";

}

}

有没有思考过,为什么访问网址http://localhost:8080/hello,就会在网页上返回Hello World ~。原因是使用@ResponseBody注解

@ResponseBody注解:

类型:方法注解、类注解位置:书写在Controller方法上或类上作用:将方法返回值直接响应给浏览器

如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

但是在上面的代码中Controller方法上或类上加的都是@RestController而不是@ResponseBody

原因:在类上添加的@RestController注解,是一个组合注解。

@RestController = @Controller + @ResponseBody

@RestController源码:

@Target({ ElementType.TYPE}) //元注解(修饰注解的注解)

@Retention(RetentionPolicy.RUNTIME) //元注解

@Documented //元注解

@Controller

@ResponseBody

public @interface RestController {

@AliasFor(

annotation = Controller.class

)

String value() default "";

}

结论:在类上添加@RestController就相当于添加了@ResponseBody注解。

类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据

方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器

字符串数据响应

@RestController

public class ResponseController {

// http://localhost:8080/hello

@RequestMapping("/hello")

public String hello(){

System.out.println("Hello World");

return "Hello World";

}

}

在浏览器中得到的响应如下:

hello world

实体数据响应

@RestController

public class ResponseController {

// http://localhost:8080/getAddr

@RequestMapping("/getAddr")

public Address getAddr(){

Address addr = new Address(); // 创建实体类对象

addr.setProvince("广东");

addr.setCity("深圳");

return addr;

}

}

实体数据响应返回的是json

{

"province": "广州",

"city": "深圳"

}

集合数据响应

@RestController

public class ResponseController {

// http://localhost:8080/listAddr

@RequestMapping("/listAddr")

public List<Address> listAddr(){

List<Address> list = new ArrayList<>();//集合对象

Address addr = new Address();

addr.setProvince("广东");

addr.setCity("深圳");

Address addr2 = new Address();

addr2.setProvince("陕西");

addr2.setCity("西安");

list.add(addr);

list.add(addr2);

return list;

}

}

还是json

[

{

"province": "广州",

"city": "深圳"

},

{

"province": "陕西",

"city": "西安"

}

]

统一响应结果

前端开发人员,如果拿到的响应数据,没有统一的规范。对前端开发人员业讲,就需要针对不同的响应数据,使用不同的解析方式。上述这种情况就会造成:开发成本高、项目不方便管理、维护起来也比较难。

上面展示了字符串、实体对象和集合的响应。虽然实体对象和集合都返回了json格式数据,都是呢,还是不规范。前后端分离程序,后端最后还是要和前端合体,因此我妹写的代码不能只是我们可以看懂,前端人员也可以看懂。因此就有一种约定

统一的返回结果使用类来描述,在这个结果中包含:

响应状态码:当前请求是成功,还是失败

状态码信息:给页面的提示信息

返回的数据:给前端响应的数据(字符串、对象、集合)

例如,

{

"code" : 1,

"msg" : "操作成功",

"data" : ...

}

老师给出了一段定义在一个实体类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);

}

}

改造刚才写的Controller:

@RestController

public class ResponseControllerDemo2 {

// http://localhost:8080/test/hello

@RequestMapping("/test/hello")

public Result hello() {

System.out.println("hello world");

return Result.success("hello world");

}

// http://localhost:8080/test/getAddr

@RequestMapping("/test/getAddr")

public Result getAddr() {

Address address = new Address();

address.setProvince("广州");

address.setCity("深圳");

return Result.success(address);

}

// http://localhost:8080/test/listAddr

@RequestMapping("/test/listAddr")

public Result listAddr() {

List<Address> list = new ArrayList<Address>();

Address address1 = new Address();

address1.setProvince("广州");

address1.setCity("深圳");

Address address2 = new Address();

address2.setProvince("陕西");

address2.setCity("西安");

list.add(address1);

list.add(address2);

return Result.success(list);

}

}

使用Postman测试:

{

"code": 1,

"msg": "success",

"data": "hello world"

}

{

"code": 1,

"msg": "success",

"data": {

"province": "广州",

"city": "深圳"

}

}

{

"code": 1,

"msg": "success",

"data": [

{

"province": "广州",

"city": "深圳"

},

{

"province": "陕西",

"city": "西安"

}

]

}

格式得到了统一

分层解耦

三层架构

介绍

在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。

单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。

这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。

刚刚跟着老师做了一个练习,我把核心代码复制一下,通过这段代码就可以明白了三层架构是什么意思

package com.yang.springbootempsystem.controller;

import com.yang.springbootempsystem.pojo.Emp;

import com.yang.springbootempsystem.pojo.Result;

import com.yang.springbootempsystem.utils.XmlParserUtils;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController

public class EmpController {

// http://localhost:8080/listEmp

@RequestMapping("/listEmp")

public Result listEmp() {

// 加载并解析emp.xml

String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();

System.out.println(file);

List<Emp> empList = XmlParserUtils.parse(file, Emp.class);

// 对数据进行处理

empList.stream().forEach(emp -> {

// <!-- 1: 男, 2: 女 -->

String gender = emp.getGender();

if ("1".equals(gender)) {

emp.setGender("男");

}else if ("2".equals(gender)) {

emp.setGender("女");

}

// <!-- 1: 讲师, 2: 班主任 , 3: 就业指导 -->

String job = emp.getJob();

if ("1".equals(job)) {

emp.setJob("讲师");

}else if ("2".equals(job)) {

emp.setJob("班主任");

}else if ("3".equals(job)) {

emp.setJob("就业指导");

}

});

// 响应数据

return Result.success(empList);

}

}

通过注释也可以看出来,这段代码有三个部分:加载数据、处理数据和响应数据。其实把这三段写在一个文件里面是不妥的,增加了代码的阅读困难(这是一句套话了)。三层架构就是处理了这个事情。

那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:

数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。逻辑处理:负责业务逻辑处理的代码。请求处理、响应数据:负责,接收页面的请求,给页面响应数据。

按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:

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

基于三层架构的程序执行流程:

前端发起的请求,由Controller层接收(Controller响应数据给前端)Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?

答案:不会影响。 (程序的扩展性、维护性变得更好了)

三层架构并不是很厉害的技术,相当于代码变得易读、易维护。

拆分代码

我们使用三层架构思想,来改造下之前的程序:

控制层包名:xxxx.controller业务逻辑层包名:xxxx.service数据访问层包名:xxxx.dao

创建三个包

**控制层:**接收前端发送的请求,对请求进行处理,并响应数据

@RestController

public class EmpController {

//业务层对象

private EmpService empService = new EmpServiceA();

@RequestMapping("/listEmp")

public Result list(){

//1. 调用service层, 获取数据

List<Emp> empList = empService.listEmp();

//3. 响应数据

return Result.success(empList);

}

}

**业务逻辑层:**处理具体的业务逻辑

业务接口

//业务逻辑接口(制定业务标准)

public interface EmpService {

//获取员工列表

public List<Emp> listEmp();

}

业务实现类

//业务逻辑实现类(按照业务标准实现)

public class EmpServiceA implements EmpService {

//dao层对象

private EmpDao empDao = new EmpDaoA();

@Override

public List<Emp> listEmp() {

//1. 调用dao, 获取数据

List<Emp> empList = empDao.listEmp();

//2. 对数据进行转换处理 - gender, job

empList.stream().forEach(emp -> {

//处理 gender 1: 男, 2: 女

String gender = emp.getGender();

if("1".equals(gender)){

emp.setGender("男");

}else if("2".equals(gender)){

emp.setGender("女");

}

//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导

String job = emp.getJob();

if("1".equals(job)){

emp.setJob("讲师");

}else if("2".equals(job)){

emp.setJob("班主任");

}else if("3".equals(job)){

emp.setJob("就业指导");

}

});

return empList;

}

}

**数据访问层:**负责数据的访问操作,包含数据的增、删、改、查

数据访问接口

//数据访问层接口(制定标准)

public interface EmpDao {

//获取员工列表数据

public List<Emp> listEmp();

}

数据访问实现类

//数据访问实现类

public class EmpDaoA implements EmpDao {

@Override

public List<Emp> listEmp() {

//1. 加载并解析emp.xml

String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();

System.out.println(file);

List<Emp> empList = XmlParserUtils.parse(file, Emp.class);

return empList;

}

}

三层架构的好处:

复用性强便于维护利用扩展

分解耦合

耦合问题

首先需要了解软件开发涉及到的两个概念:内聚和耦合。

内聚:软件中各个功能模块内部的功能联系。

耦合:衡量软件中各个层/模块之间的依赖、关联的程度。

软件设计原则:高内聚低耦合。

高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 “高内聚”。

低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。

耦合和内聚,在我学习python的时候,老师提到过。

高内聚,是让一个模块中各个元素联系紧密;低内聚是模块与模块之间没有关系。越低越好。

在我做的上面的项目中,高内聚的体现就是EmpController.java文件就是用来请求数据,响应数据的;EmpServiceA.java文件用于处理逻辑业务的,而EmpDao.java就是专门加载数据的。三个文件各司其职。

也有耦合的体现,如果我要变更业务,把EmpServiceA.java变成EmpServiceB.java时,那么我还需要在控制层EmpController.java文件中修改代码 private EmpService empService = new EmpServiceA();改成 new EmpServiceB();。虽然不是很麻烦,但是spring提供了更好的解决方法

高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。

解耦思路

我们的解决思路是:

提供一个容器,容器中存储一些对象(例:EmpService对象)controller程序从容器中获取EmpService类型的对象

在上面或者之前写的代码中,需要什么对象时,就直接new一个,private EmpService empService = new EmpServiceA();,而现在呢,把这些对象都放进一个容器中,这时,需要什么对象时,不是我们去new,而是程序自己在这个容器中找。

控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器

依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

程序运行时需要某个资源,此时容器就为其提供这个资源。

例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象

IOC容器中创建、管理的对象,称之为:bean对象

这个这么理解呢,控制反转就是不需要我去创建对象,而是容器自己创建对象,程序自己去找要用到的这个对象、

依赖注入,这个容器会自己new对象,并且给程序。

控制反转是程序去容器中找对象,而依赖注入容器给程序提供对象

IOC&DI

IOC&DI入门

任务:完成Controller层、Service层、Dao层的代码解耦

思路:

删除Controller层、Service层中new对象的代码Service层及Dao层的实现类,交给IOC容器管理为Controller及Service注入运行时依赖的对象

Controller程序中注入依赖的Service层对象Service程序中注入依赖的Dao层对象

步骤:

第1步:删除Controller层、Service层中new对象的代码

第2步:Service层及Dao层的实现类,交给IOC容器管理

使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理

第3步:为Controller及Service注入运行时依赖的对象

使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

完整的三层代码:

Controller层:

@RestController

public class EmpController {

@Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量

private EmpService empService ;

@RequestMapping("/listEmp")

public Result list(){

//1. 调用service, 获取数据

List<Emp> empList = empService.listEmp();

//3. 响应数据

return Result.success(empList);

}

}

Service层:

@Component //将当前对象交给IOC容器管理,成为IOC容器的bean

public class EmpServiceA implements EmpService {

@Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量

private EmpDao empDao ;

@Override

public List<Emp> listEmp() {

//1. 调用dao, 获取数据

List<Emp> empList = empDao.listEmp();

//2. 对数据进行转换处理 - gender, job

empList.stream().forEach(emp -> {

//处理 gender 1: 男, 2: 女

String gender = emp.getGender();

if("1".equals(gender)){

emp.setGender("男");

}else if("2".equals(gender)){

emp.setGender("女");

}

//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导

String job = emp.getJob();

if("1".equals(job)){

emp.setJob("讲师");

}else if("2".equals(job)){

emp.setJob("班主任");

}else if("3".equals(job)){

emp.setJob("就业指导");

}

});

return empList;

}

}

Dao层:

@Component //将当前对象交给IOC容器管理,成为IOC容器的bean

public class EmpDaoA implements EmpDao {

@Override

public List<Emp> listEmp() {

//1. 加载并解析emp.xml

String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();

System.out.println(file);

List<Emp> empList = XmlParserUtils.parse(file, Emp.class);

return empList;

}

}

对以上步骤做个总结。首先 要清楚用到的两个注解

@Component // 当前类交给IOC容器管理,成为IOC容器中的bean

@Autowired // 程序在运行时,ioc容器会提供该类型的bean对象,并肤质给该对象

通过给实现类添加@Component 注解,把当前类交给IOC容器管理。

@Component // 当前类交给IOC容器管理,成为IOC容器中的bean

public class EmpServiceB implements EmpService {

@Override

public List<Emp> listEmp() {

...

}

}

随后用@Autowired注解进行依赖注入。要让谁去创建EmpServiceB对象,就去给这个变量去注解

@RestController

public class EmpController {

...

@Autowired // 程序在运行时,ioc容器会提供该类型的bean对象,并肤质给该对象

private EmpService empService;

...

}

IOC详解

bean的声明

前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。

在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component

而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:

@Controller (标注在控制层类上)@Service (标注在业务层类上)@Repository (标注在数据访问层类上)

@Controller@Service @Repository其实和@Component 的作用都一样,只不过前面的更有标识。被@Controller标注的类,当程序员打开这个文件时,就会像条件反射性的明白,这段代码属于控制层,处理请求响应的。

修改入门案例代码中的EmpServiceA类

Service层:

@Service

public class EmpServiceA implements EmpService {

@Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量

private EmpDao empDao ;

@Override

public List<Emp> listEmp() {

//1. 调用dao, 获取数据

List<Emp> empList = empDao.listEmp();

//2. 对数据进行转换处理 - gender, job

empList.stream().forEach(emp -> {

//处理 gender 1: 男, 2: 女

String gender = emp.getGender();

if("1".equals(gender)){

emp.setGender("男");

}else if("2".equals(gender)){

emp.setGender("女");

}

//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导

String job = emp.getJob();

if("1".equals(job)){

emp.setJob("讲师");

}else if("2".equals(job)){

emp.setJob("班主任");

}else if("3".equals(job)){

emp.setJob("就业指导");

}

});

return empList;

}

}

在类的前面添加了注解@Service

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

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

在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。

<code>@Service(value = "ser")

public class EmpServiceC implements EmpService { -- -->

...

}

给EmpServiceC类指定了一个小名,ser。这个用的不多。当作了解。

注意事项:

声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。

组件扫描

问题:使用前面学习的四个注解声明的bean,一定会生效吗?

答案:不一定。(原因:bean想要生效,还需要被组件扫描)

如果修改目录结构,那么bean对象就不会生效。在上面,dao目录是在src/java/com.yang/ 的目录下面。如果改变dao目录位置src/java/dao程序运行会报错

Description:

Field empDao in com.yang.springbootempsystem.service.impl.EmpServiceC required a bean of type 'Dao.EmpDao' that could not be found.

The injection point has the following annotations:

- @org.springframework.beans.factory.annotation.Autowired(required=true)

需要一个EmpDao类型的bean,但是找不到这个bean,

为什么没有找到bean对象呢?

使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

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

文件的目录发生了改变,那么springboot是无法扫描的

解决方案:手动添加@ComponentScan注解,指定要扫描的包 (仅做了解,不推荐

@ComponentScan({ "dao", "com.yang.springbootempsystem"})

@SpringBootApplication

public class SpringBootEmpsystemApplication {

public static void main(String[] args) {

SpringApplication.run(SpringBootEmpsystemApplication.class, args);

}

}

@ComponentScan({"dao", "com.yang.springbootempsystem"})告诉springboot要扫描的位置

推荐做法(如下图):

将我们定义的controller,service,dao这些包呢,都放在引导类所在包com.itheima的子包下,这样我们定义的bean就会被自动的扫描到

按照maven生成的工程目录规范开发

DI详解

上一小节我们讲解了控制反转IOC的细节,接下来呢,我们学习依赖注解DI的细节。

依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。

在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。

@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。

上面介绍了什么是依赖注入,依赖注入就是为应用程序提供运行时所依赖的对象

那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?

@Service

public class EmpServiceA implements EmpService {

@Override

public List<Emp> listEmp() {

}

}

@Service

class EmpServiceB implements EmpService {

@Override

public List<Emp> listEmp() {

}

}

@Service

class EmpServiceC implements EmpService {

@Override

public List<Emp> listEmp() {

}

}

这样就是同时依赖三个相同的bean,运行程序会报错。

Description:

Field empService in com.yang.springbootempsystem.controller.EmpController required a single bean, but 3 were found:

- empServiceA: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceA.class]

- empServiceB: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceB.class]

- ser: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceC.class]

This may be due to missing parameter name information

程序运行会报错

如何解决上述问题呢?Spring提供了以下几种解决方案:

@Primary

@Qualifier

@Resource

使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

@Primary

@Service

public class EmpServiceC implements EmpService {

@Override

public List<Emp> listEmp() {

}

}

现在我只让EmpServiceC类生效

使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

@Qualifier注解不能单独使用,必须配合@Autowired使用

使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

面试题 : @Autowird 与 @Resource的区别

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

e {

@Override

public List<Emp> listEmp() {

}

}

这样就是同时依赖三个相同的bean,运行程序会报错。

Description:

Field empService in com.yang.springbootempsystem.controller.EmpController required a single bean, but 3 were found:

- empServiceA: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceA.class]

- empServiceB: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceB.class]

- ser: defined in file [E:\yang\专业\Java Code\2.Java-web\code\day05-SpringBootWeb请求响应\springboot-empsystem\target\classes\com\yang\springbootempsystem\service\impl\EmpServiceC.class]

This may be due to missing parameter name information

> - 程序运行会报错

>

> 如何解决上述问题呢?Spring提供了以下几种解决方案:

>

> - @Primary

>

> - @Qualifier

>

> - @Resource

>

> 使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

```java

@Primary

@Service

public class EmpServiceC implements EmpService {

@Override

public List<Emp> listEmp() {

}

}

现在我只让EmpServiceC类生效

使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

@Qualifier注解不能单独使用,必须配合@Autowired使用

使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

面试题 : @Autowird 与 @Resource的区别

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。