上面说过,浏览器向服务端发送请求,服务端会给浏览器发送出响应,无论是哪种,都包含三部分。这一章,依旧围绕这部分内容
请求
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")
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 + '\'' +
", 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是一个实体类)
复杂实体对象的封装,需要遵守如下规则:
- 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。
复杂实体对象就是实体类中的一个或多个属性还是一个实体类。
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 + '\'' +
", city='" + city + '\'' +
'}';
}
}
- 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 + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
Controller方法:
@RestController
public class RequestController {
//实体参数:复杂实体对象
@RequestMapping("/complexPojo")
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'}}
需要注意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方法:
@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='北京'}}
路径参数
传统的开发中请求参数是放在请求体(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的名字。如果没有指定,默认为类名首字母小写。
@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是按照名称注入