上面说过,浏览器向服务端发送请求,服务端会给浏览器发送出响应,无论是哪种,都包含三部分。这一章,依旧围绕这部分内容
请求
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;}}
三层架构的好处:
- 复用性强
- 便于维护
- 利用扩展
分解耦合
耦合问题
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
- 内聚:软件中各个功能模块内部的功能联系。
- 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 “高内聚”。
低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。
耦合和内聚,在我学习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是按照名称注入
版权归原作者 骨Zi里的傲慢欢hhh 所有, 如有侵权,请联系我们删除。