day05-SpringBootWeb请求响应学习笔记

news2024/11/14 13:24:57

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

请求

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=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 + '\'' +
                ", 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层
Service层
Dao层
数据
  • 前端发起的请求,由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;
    }
}

三层架构的好处:

  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层:
@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是按照名称注入

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2047781.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot——整合Swagger

目录 Swagger Swagger工具集 Swagger注解 项目总结 新建SpringBoot项目 pom.xml Swagger2Config配置类 User实体类 UserController控制器 项目测试 添加用户 修改用户 查询用户 删除用户 Swagger Swagger是一款基于RESTful接口的用于文档在线自动生成和功能测试的开…

揭开虚拟与现实的帷幕:二进制世界与道

本章将带领读者进入一个结合科学与哲学的思维世界&#xff0c;从一个全新的视角探讨二进制世界的概念&#xff0c;结合超弦理论和老子的“道”哲学&#xff0c;深入理解计算机底层的运行原理及其与宇宙本质的联系。通过回顾经典电影《黑客帝国》以及最新的人工智能发展&#xf…

Android经典实战之Kotlin 2.0 迁移指南:全方位优化与新特性解析

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 Kotlin 2.0 迁移指南&#xff1a;开发者如何迎接新时代 Kotlin 2.0&#xff0c;这个备受期待的版本&#xff0c;终于在 JetBrains 的精心打磨下…

前端各种文本文件预览 文本编辑excel预览编辑 pdf预览word预览 excel下载pdf下载word下载

前端各种文本文件预览 文本编辑excel预览编辑 pdf预览word预览 excel下载pdf下载word下载 各种文本文件预览&#xff08;pdf, xlsx, docx, cpp, java, sql, py, vue, html, js, json, css, xml, rust, md, txt, log, fa, fasta, tsv, csv 等各种文本文件&#xff09; 其中 除p…

【LeetCode Cookbook(C++ 描述)】一刷二叉树综合(上)

目录 LeetCode #226&#xff1a;Invert Binary Tree 翻转二叉树「遍历」「分而治之」广度优先搜索&#xff1a;层序遍历 LeetCode #101&#xff1a;Symmetric Tree 对称二叉树递归法迭代法 LeetCode #100&#xff1a;Same Tree 相同的树递归法迭代法 LeetCode #559&#xff1a;…

万能钥匙:解锁 C++ 模板的无限可能

1.泛型编程 1.1:交换两个数(C语言) 1.2:交换两个数(C) 1.3:泛型编程 2:函数模板 2.1:函数模板的概念 2.2:函数模板的格式 ​编辑 2.3:函数模板的原理 2.4:模板的实例化 2.4.1:隐式实例化 2.4.2:显式实例化:在函数名后的<>中指定模板参数的实际类型. 2.4.2.1…

Unidbg使用指南

Unidbg使用指南 简介使用Unidbg补环境仅含C语言C调用 Java 实操——车智赢在unidbg实现执行so中的方法附——关于引用数据类型的转换附——静态注册和动态注册模板静态注册动态注册 现在很多的app使用了so加密&#xff0c;以后会越来越多。爬虫工程师可能会直接逆向app&#xf…

黑马前端——days09_css

案例 1 页面框架文件 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv"X-UA-Compati…

Ubuntu20.04如何安装配置JDK

资源准备 官方下载地址&#xff08;根据自己的系统版本选择不同版本进行下载即可&#xff09;&#xff1a;Java Downloads | Oracle 如无特殊需要可直接移步至下方JDK1.8安装包 https://download.csdn.net/download/qq_43439214/89646731 安装步骤 创建Java目录 sudo mkdir …

jmeter安装及环境变量配置、Jmeter目录介绍和界面详解

一 JMeter简介 Apache JMeter是100%纯JAVA桌面应用程序&#xff0c;被设计为用于测试客户端/服务端结构的软件(例如web应用程序)。它可以用来测试静态和动态资源的性能&#xff0c;例如&#xff1a;静态文件&#xff0c;Java Servlet,CGI Scripts,Java Object,数据库和FTP服务器…

【已解决】在进行模型量化推理的过程中遇到的错误以及解决方法

①在使用vLLM推理模型时&#xff0c;出现&#xff1a; Error in calling custom op rms_norm: _OpNamespace _C object has no attribute rms_norm 尝试众多解决方法之后&#xff0c;包括重新安装 pip install vllm0.5.0 对我有用的解决方法&#xff1a; 修改子目录下的vll…

【2024最新】Windows系统上NodeJS安装及环境配置图文教程

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境&#xff0c;允许在服务器端运行 JavaScript 代码。它采用事件驱动、非阻塞 I/O 模型&#xff0c;非常适合构建高性能的网络应用程序。Node.js 提供了一系列内置模块&#xff0c;支持异步编程&#xff0c;易于扩展&…

机器学习:knn算法实现图像识别

1、概述 使用K-近邻&#xff08;K-Nearest Neighbors, KNN&#xff09;算法对手写数字进行识别的过程。通过读取一张包含多个手写数字的图片&#xff0c;将其分割成单独的数字图像&#xff0c;并将其作为训练和测试数据集。 2、数据处理思路 1、图像分割该数据有50行100列&am…

手机设备IP地址切换:方法、应用与注意事项

在当今数字化时代&#xff0c;手机已成为我们日常生活中不可或缺的一部分。无论是工作、学习还是娱乐&#xff0c;手机都扮演着重要角色。然而&#xff0c;随着网络环境的日益复杂&#xff0c;有时我们需要切换手机设备的IP地址以满足特定的需求&#xff0c;如保护隐私、绕过地…

算法笔记:空间填充曲线

空间填充曲线&#xff08;Space-filling curve&#xff09;是一种数学曲线&#xff0c;它可以无间断地覆盖一个多维空间的每一个点&#xff0c;从而实现从一维到多维的映射。用以解决连续与离散空间之间的映射问题。空间填充曲线的应用广泛&#xff0c;包括图像处理、地理信息系…

基于微信小程序的诗词智能学习系统的设计与实现(全网独一无二,24年最新定做)

文章目录 前言&#xff1a; 博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为…

dos攻击漏洞思路小结

前言 想挖掘src拒绝服务类型的漏洞&#xff0c;搜索了一圈社区相关文章较少&#xff0c;这里根据自己的一些实战案例归纳思路来抛砖引玉&#xff0c;希望能对各位师傅有所帮助&#xff01; 从黑盒视角搭配实际场景&#xff0c;说明如何具体操作能够快速的挖掘拒绝服务漏洞。 …

vue3中使用useStore(),返回undefined的踩坑记录

vue3中使用useStore()&#xff0c;返回undefined&#xff0c;排查后&#xff0c;记录一下的踩坑记录。 总结为&#xff0c;三检查&#xff1a; 1、一检查版本 在package.json中检查&#xff0c;vuex是否正常引入&#xff1a; 版本也要确认一下&#xff1a; vue3对应vuex4的…

使用光流进行相机运动估计

文章目录 基本相机移动区分动作的核心思想了解代码参考 基本相机移动 从我的非专业角度来看&#xff0c;尽管已知的摄像机运动有多种&#xff0c;但我们应该概述其中三种&#xff1a; 一种是将摄像机安装在轨道上并移动——卡车、移动式摄影车、基座摄像机停留在同一位置并旋…

MySQL中的distinct和group by哪个效率更高?

前言 大家好&#xff0c;我是月夜枫~~ 一、distinct和group by的区别 1.1.作用方式和应用场景 ‌group by和‌distinct的主要区别在于它们的作用方式和应用场景。 group by用于对数据进行分组和聚合操作&#xff0c;通常与聚合函数&#xff08;如COUNT、SUM、AVG等&#xf…