0


day05-SpringBootWeb请求响应学习笔记

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

请求

Postman

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

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

  • 弊端:在浏览器地址栏中输入地址这种方式都是GET请求,如何我们要用到POST请求怎么办呢? - 要解决POST请求,需要程序员自己编写前端代码(比较麻烦)

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

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

  • Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。> Postman原是Chrome浏览器的插件,可以模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求> > 使用Postman还可以在发起请求时,携带一些请求参数、请求头等信息
  • 作用:常用于进行接口测试
  • 特征- 简单- 实用- 美观- 大方

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

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

简单参数

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

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

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

在上面的url中

name=Tom

age=10

就是一种请求数据。这个数据单一,也就是普通数据。

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

Tom

age

原始方式

在原始的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")publicStringsimpleParam1(HttpServletRequest request){// 获取请求参数String name = request.getParameter("name");String ageStr = request.getParameter("age");// 类型转换int age =Integer.parseInt(ageStr);System.out.println(name +","+ age);// Tom,22return"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")publicStringsimpleParam2(String name,Integer age){// 获取请求参数System.out.println(name +","+ age);// Tom,22return"ok";}

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

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

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

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

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

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

@RequestMapping("/simpleParam-springboot")publicStringsimpleParam2(@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")publicStringsimpleParam2(@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")publicStringsimpleParam2(String name,Integer age,String gender ...){return"ok";}

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

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

简单的实体参数

定义POJO实体类:

publicclassUser{privateString name;privateInteger age;publicStringgetName(){return name;}publicvoidsetName(String name){this.name = name;}publicIntegergetAge(){return age;}publicvoidsetAge(Integer age){this.age = age;}@OverridepublicStringtoString(){return"User{"+"name='"+ name +'\''+", age="+ age +'}';}}

Controller方法:

@RestController
public class RequestController {
    //实体参数:简单实体对象
    @RequestMapping("/simplePojo")
    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}
  • 当请求参数不一致时。http://localhost:8080/simplePojo?name=zhangsan&agew=22浏览器还会返回ok,此时看控制台System.out.println(user);// User{name='zhangsan', age=null}

复杂的实体对象

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

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

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

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

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

publicclassUser{privateString name;privateInteger age;privateAddress address;//地址对象...}

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

定义POJO实体类:

  • Address实体类
publicclassAddress{privateString province;privateString city;publicStringgetProvince(){return province;}publicvoidsetProvince(String province){this.province = province;}publicStringgetCity(){return city;}publicvoidsetCity(String city){this.city = city;}@OverridepublicStringtoString(){return"Address{"+"province='"+ province +'\''+", city='"+ city +'\''+'}';}}
  • User实体类
publicclassUser{privateString name;privateInteger age;privateAddress address;//地址对象publicStringgetName(){return name;}publicvoidsetName(String name){this.name = name;}publicIntegergetAge(){return age;}publicvoidsetAge(Integer age){this.age = age;}publicAddressgetAddress(){return address;}publicvoidsetAddress(Address address){this.address = address;}@OverridepublicStringtoString(){return"User{"+"name='"+ name +'\''+", age="+ age +", address="+ address +'}';}}

Controller方法:

@RestControllerpublicclassRequestController{//实体参数:复杂实体对象@RequestMapping("/complexPojo")publicStringcomplexPojo(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'}}

需要注意url中的参数

address.province=

address.city=

应该和Address类中的属性值保持一致。

数组集合参数

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

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

数组参数

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

@RestControllerpublicclassRequestController{//数组集合参数@RequestMapping("/arrayParam")publicStringarrayParam(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方法:

@RestControllerpublicclassRequestController{//日期时间参数@RequestMapping("/dateParam")publicStringdateParam(@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方法:

@RestControllerpublicclassRequestController{//JSON参数@RequestMapping("/jsonParam")publicStringjsonParam(@RequestBodyUser 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='北京'}}

路径参数

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

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

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

上面两个url中,

/1

/1/0

就是路径参数,后端是需要拿到的

路径参数:

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

Controller方法:

@RestControllerpublicclassRequestController{//路径参数@RequestMapping("/path/{id}")publicStringpathParam(@PathVariableInteger id){System.out.println(id);return"OK";}}

Postman测试:

http://localhost:8080/pathParam/1
System.out.println(id);// 1

也可以传递多个路径

@RestControllerpublicclassRequestController{//路径参数@RequestMapping("/path/{id}/{name}")publicStringpathParam2(@PathVariableInteger id,@PathVariableString 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@ResponseBodypublic@interfaceRestController{@AliasFor(
        annotation =Controller.class)Stringvalue()default"";}

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

  • 类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据 - 方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器

字符串数据响应

@RestControllerpublicclassResponseController{// http://localhost:8080/hello@RequestMapping("/hello")publicStringhello(){System.out.println("Hello World");return"Hello World";}}

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

hello world

实体数据响应

@RestControllerpublicclassResponseController{// http://localhost:8080/getAddr@RequestMapping("/getAddr")publicAddressgetAddr(){Address addr =newAddress();// 创建实体类对象
        addr.setProvince("广东");
        addr.setCity("深圳");return addr;}}

实体数据响应返回的是json

{"province":"广州","city":"深圳"}

集合数据响应

@RestControllerpublicclassResponseController{// http://localhost:8080/listAddr@RequestMapping("/listAddr")publicList<Address>listAddr(){List<Address> list =newArrayList<>();//集合对象Address addr =newAddress();
        addr.setProvince("广东");
        addr.setCity("深圳");Address addr2 =newAddress();
        addr2.setProvince("陕西");
        addr2.setCity("西安");

        list.add(addr);
        list.add(addr2);return list;}}

还是json

[{"province":"广州","city":"深圳"},{"province":"陕西","city":"西安"}]

统一响应结果

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

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

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

  • 响应状态码:当前请求是成功,还是失败
  • 状态码信息:给页面的提示信息
  • 返回的数据:给前端响应的数据(字符串、对象、集合)

例如,

{"code":1,"msg":"操作成功","data":...}

老师给出了一段定义在一个实体类Result来包含以上信息的代码。代码如下:

publicclassResult{privateInteger code;//响应码,1 代表成功; 0 代表失败privateString msg;//响应码 描述字符串privateObject data;//返回的数据publicResult(){}publicResult(Integer code,String msg,Object data){this.code = code;this.msg = msg;this.data = data;}publicIntegergetCode(){return code;}publicvoidsetCode(Integer code){this.code = code;}publicStringgetMsg(){return msg;}publicvoidsetMsg(String msg){this.msg = msg;}publicObjectgetData(){return data;}publicvoidsetData(Object data){this.data = data;}//增删改 成功响应(不需要给前端返回数据)publicstaticResultsuccess(){returnnewResult(1,"success",null);}//查询 成功响应(把查询结果做为返回数据响应给前端)publicstaticResultsuccess(Object data){returnnewResult(1,"success",data);}//失败响应publicstaticResulterror(String msg){returnnewResult(0,msg,null);}}

改造刚才写的Controller:

@RestControllerpublicclassResponseControllerDemo2{//    http://localhost:8080/test/hello@RequestMapping("/test/hello")publicResulthello(){System.out.println("hello world");returnResult.success("hello world");}//    http://localhost:8080/test/getAddr@RequestMapping("/test/getAddr")publicResultgetAddr(){Address address =newAddress();
        address.setProvince("广州");
        address.setCity("深圳");returnResult.success(address);}//    http://localhost:8080/test/listAddr@RequestMapping("/test/listAddr")publicResultlistAddr(){List<Address> list =newArrayList<Address>();Address address1 =newAddress();
        address1.setProvince("广州");
        address1.setCity("深圳");Address address2 =newAddress();
        address2.setProvince("陕西");
        address2.setCity("西安");

        list.add(address1);
        list.add(address2);returnResult.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":"西安"}]}

格式得到了统一

分层解耦

三层架构

介绍

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

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

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

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

packagecom.yang.springbootempsystem.controller;importcom.yang.springbootempsystem.pojo.Emp;importcom.yang.springbootempsystem.pojo.Result;importcom.yang.springbootempsystem.utils.XmlParserUtils;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.List;@RestControllerpublicclassEmpController{// http://localhost:8080/listEmp@RequestMapping("/listEmp")publicResultlistEmp(){// 加载并解析emp.xmlString 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("男");}elseif("2".equals(gender)){
                emp.setGender("女");}// <!-- 1: 讲师, 2: 班主任 , 3: 就业指导 -->String job = emp.getJob();if("1".equals(job)){
                emp.setJob("讲师");}elseif("2".equals(job)){
                emp.setJob("班主任");}elseif("3".equals(job)){
                emp.setJob("就业指导");}});// 响应数据returnResult.success(empList);}}

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

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

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

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

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

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

#mermaid-svg-wAv9wh5bajCek2TB {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wAv9wh5bajCek2TB .error-icon{fill:#552222;}#mermaid-svg-wAv9wh5bajCek2TB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wAv9wh5bajCek2TB .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-wAv9wh5bajCek2TB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wAv9wh5bajCek2TB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wAv9wh5bajCek2TB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wAv9wh5bajCek2TB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wAv9wh5bajCek2TB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wAv9wh5bajCek2TB .marker.cross{stroke:#333333;}#mermaid-svg-wAv9wh5bajCek2TB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wAv9wh5bajCek2TB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wAv9wh5bajCek2TB .cluster-label text{fill:#333;}#mermaid-svg-wAv9wh5bajCek2TB .cluster-label span{color:#333;}#mermaid-svg-wAv9wh5bajCek2TB .label text,#mermaid-svg-wAv9wh5bajCek2TB span{fill:#333;color:#333;}#mermaid-svg-wAv9wh5bajCek2TB .node rect,#mermaid-svg-wAv9wh5bajCek2TB .node circle,#mermaid-svg-wAv9wh5bajCek2TB .node ellipse,#mermaid-svg-wAv9wh5bajCek2TB .node polygon,#mermaid-svg-wAv9wh5bajCek2TB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wAv9wh5bajCek2TB .node .label{text-align:center;}#mermaid-svg-wAv9wh5bajCek2TB .node.clickable{cursor:pointer;}#mermaid-svg-wAv9wh5bajCek2TB .arrowheadPath{fill:#333333;}#mermaid-svg-wAv9wh5bajCek2TB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wAv9wh5bajCek2TB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wAv9wh5bajCek2TB .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-wAv9wh5bajCek2TB .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-wAv9wh5bajCek2TB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wAv9wh5bajCek2TB .cluster text{fill:#333;}#mermaid-svg-wAv9wh5bajCek2TB .cluster span{color:#333;}#mermaid-svg-wAv9wh5bajCek2TB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wAv9wh5bajCek2TB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

        用户界面 
      


        Controller层 
      


        Service层 
      


        Dao层 
      


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

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

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

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

拆分代码

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

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

创建三个包

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

@RestControllerpublicclassEmpController{//业务层对象privateEmpService empService =newEmpServiceA();@RequestMapping("/listEmp")publicResultlist(){//1. 调用service层, 获取数据List<Emp> empList = empService.listEmp();//3. 响应数据returnResult.success(empList);}}

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

  • 业务接口
//业务逻辑接口(制定业务标准)publicinterfaceEmpService{//获取员工列表publicList<Emp>listEmp();}
  • 业务实现类
//业务逻辑实现类(按照业务标准实现)publicclassEmpServiceAimplementsEmpService{//dao层对象privateEmpDao empDao =newEmpDaoA();@OverridepublicList<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("男");}elseif("2".equals(gender)){
                emp.setGender("女");}//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导String job = emp.getJob();if("1".equals(job)){
                emp.setJob("讲师");}elseif("2".equals(job)){
                emp.setJob("班主任");}elseif("3".equals(job)){
                emp.setJob("就业指导");}});return empList;}}

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

  • 数据访问接口
//数据访问层接口(制定标准)publicinterfaceEmpDao{//获取员工列表数据publicList<Emp>listEmp();}
  • 数据访问实现类
//数据访问实现类publicclassEmpDaoAimplementsEmpDao{@OverridepublicList<Emp>listEmp(){//1. 加载并解析emp.xmlString file =this.getClass().getClassLoader().getResource("emp.xml").getFile();System.out.println(file);List<Emp> empList =XmlParserUtils.parse(file,Emp.class);return empList;}}

三层架构的好处:

  1. 复用性强
  2. 便于维护
  3. 利用扩展

分解耦合

耦合问题

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

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

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

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

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

耦合和内聚,在我学习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层的代码解耦

  • 思路:1. 删除Controller层、Service层中new对象的代码2. Service层及Dao层的实现类,交给IOC容器管理3. 为Controller及Service注入运行时依赖的对象 - Controller程序中注入依赖的Service层对象- Service程序中注入依赖的Dao层对象
  • 步骤:第1步:删除Controller层、Service层中new对象的代码第2步:Service层及Dao层的实现类,交给IOC容器管理- 使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理第3步:为Controller及Service注入运行时依赖的对象- 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

完整的三层代码:

  • Controller层:
@RestControllerpublicclassEmpController{@Autowired//运行时,从IOC容器中获取该类型对象,赋值给该变量privateEmpService empService ;@RequestMapping("/listEmp")publicResultlist(){//1. 调用service, 获取数据List<Emp> empList = empService.listEmp();//3. 响应数据returnResult.success(empList);}}
  • Service层:
@Component//将当前对象交给IOC容器管理,成为IOC容器的beanpublicclassEmpServiceAimplementsEmpService{@Autowired//运行时,从IOC容器中获取该类型对象,赋值给该变量privateEmpDao empDao ;@OverridepublicList<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("男");}elseif("2".equals(gender)){
                emp.setGender("女");}//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导String job = emp.getJob();if("1".equals(job)){
                emp.setJob("讲师");}elseif("2".equals(job)){
                emp.setJob("班主任");}elseif("3".equals(job)){
                emp.setJob("就业指导");}});return empList;}}

Dao层:

@Component//将当前对象交给IOC容器管理,成为IOC容器的beanpublicclassEmpDaoAimplementsEmpDao{@OverridepublicList<Emp>listEmp(){//1. 加载并解析emp.xmlString 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容器中的beanpublicclassEmpServiceBimplementsEmpService{@OverridepublicList<Emp>listEmp(){...}}

随后用

@Autowired

注解进行依赖注入。要让谁去创建

EmpServiceB

对象,就去给这个变量去注解

@RestControllerpublicclassEmpController{...@Autowired// 程序在运行时,ioc容器会提供该类型的bean对象,并肤质给该对象privateEmpService 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层:
@ServicepublicclassEmpServiceAimplementsEmpService{@Autowired//运行时,从IOC容器中获取该类型对象,赋值给该变量privateEmpDao empDao ;@OverridepublicList<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("男");}elseif("2".equals(gender)){
                emp.setGender("女");}//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导String job = emp.getJob();if("1".equals(job)){
                emp.setJob("讲师");}elseif("2".equals(job)){
                emp.setJob("班主任");}elseif("3".equals(job)){
                emp.setJob("就业指导");}});return empList;}}

在类的前面添加了注解

@Service

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解说明位置@Controller@Component的衍生注解标注在控制器类上@Service@Component的衍生注解标注在业务类上@Repository@Component的衍生注解标注在数据访问类上(由于与mybatis整合,用的少)@Component声明bean的基础注解不属于以上三类时,用此注解

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

@Service(value ="ser")publicclassEmpServiceCimplementsEmpService{...}

给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"})@SpringBootApplicationpublicclassSpringBootEmpsystemApplication{publicstaticvoidmain(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对象,会出现什么情况呢?

@ServicepublicclassEmpServiceAimplementsEmpService{@OverridepublicList<Emp>listEmp(){}}@ServiceclassEmpServiceBimplementsEmpService{@OverridepublicList<Emp>listEmp(){}}@ServiceclassEmpServiceCimplementsEmpService{@OverridepublicList<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@ServicepublicclassEmpServiceCimplementsEmpService{@OverridepublicList<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是按照名称注入
标签: 学习 笔记

本文转载自: https://blog.csdn.net/qq_45348840/article/details/141276557
版权归原作者 骨Zi里的傲慢欢hhh 所有, 如有侵权,请联系我们删除。

“day05-SpringBootWeb请求响应学习笔记”的评论:

还没有评论