08.SpringBoot请求相应

文章目录

  • [1 请求](#1 请求)
    • [1.1 Postman](#1.1 Postman)
    • [1.2 简单参数](#1.2 简单参数)
      • [1.2.1 原始方式](#1.2.1 原始方式)
      • [1.2.2 SpringBoot方式](#1.2.2 SpringBoot方式)
      • [1.2.3 参数名不一致](#1.2.3 参数名不一致)
    • [1.3 实体参数](#1.3 实体参数)
      • [1.3.1 简单实体对象](#1.3.1 简单实体对象)
      • [1.3.2 复杂实体对象](#1.3.2 复杂实体对象)
    • [1.4 数组集合参数](#1.4 数组集合参数)
      • [1.4.1 数组](#1.4.1 数组)
      • [1.4.2 集合](#1.4.2 集合)
    • [1.5 日期参数](#1.5 日期参数)
    • [1.6 JSON参数](#1.6 JSON参数)
    • [1.7 路径参数](#1.7 路径参数)
  • [2 响应](#2 响应)
    • [2.1 @ResponseBody注解](#2.1 @ResponseBody注解)
    • [2.2 统一响应结果](#2.2 统一响应结果)
  • [3 分层解耦](#3 分层解耦)
    • [3.1 三层架构](#3.1 三层架构)
      • [3.1.1 介绍](#3.1.1 介绍)
    • [3.2 分层解耦](#3.2 分层解耦)
      • [3.2.1 耦合问题](#3.2.1 耦合问题)
      • [3.2.2 解耦思路](#3.2.2 解耦思路)
    • [3.3 IOC&DI](#3.3 IOC&DI)
      • [3.3.1 bean对象声明](#3.3.1 bean对象声明)
      • [3.3.2 IOC详解](#3.3.2 IOC详解)
      • [3.3.3 DI详解](#3.3.3 DI详解)

1 请求

1.1 Postman

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

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

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

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

  • 特征

    • 简单
    • 实用
    • 美观
    • 大方

1.2 简单参数

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

而在后端程序中,接收普通参数数据的方式有两种:

  1. 原始方式
  2. SpringBoot方式

1.2.1 原始方式

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

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

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

json 复制代码
//根据指定的参数名获取请求参数的数据值
String  request.getParameter("参数名")
java 复制代码
@RestController
public class RequestController {
    //原始方式
    @RequestMapping("/simpleParam")
    public String simpleParam(HttpServletRequest request){
        // http://localhost:8080/simpleParam?name=Tom&age=10
        // 请求参数: name=Tom&age=10   (有2个请求参数)
        // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
        // 第2个请求参数: age=10     参数名:age , 参数值:10

        String name = request.getParameter("name");//name就是请求参数名
        String ageStr = request.getParameter("age");//age就是请求参数名

        int age = Integer.parseInt(ageStr);//需要手动进行类型转换
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

1.2.2 SpringBoot方式

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

java 复制代码
@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=10
    // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
    // 第2个请求参数: age=10     参数名:age , 参数值:10
    
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

不论是GET请求还是POST请求,对于简单参数来讲,只要保证请求参数名和Controller方法中的形参名保持一致,就可以获取到请求参数中的数据值。

1.2.3 参数名不一致

对于简单参数 来讲,请求参数名controller方法中的形参名不一致时,无法接收到请求数据,当我们在开发中,遇到了这种请求参数名controller方法中的形参名不相同,可以使用Spring提供的@RequestParam注解完成映射

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

java 复制代码
@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=20
    // 请求参数名:name

    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(@RequestParam("name") String username , Integer age ){
        System.out.println(username+"  :  "+age);
        return "OK";
    }
}

注意:

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

如果该参数是可选的,可以将required属性设置为false

java 复制代码
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}

1.3 实体参数

在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。

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

1.3.1 简单实体对象

定义POJO类:

java 复制代码
package com.yongzhi.springbootwebreqresp.pojo;

public class User {
    private String name;
    private int age;

    private Address address;


    public User() {
    }

    public User(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     * @return address
     */
    public Address getAddress() {
        return address;
    }

    /**
     * 设置
     * @param address
     */
    public void setAddress(Address address) {
        this.address = address;
    }

    public String toString() {
        return "User{name = " + name + ", age = " + age + ", address = " + address + "}";
    }
}

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //实体参数:简单实体对象
    @RequestMapping("/simplePojo")
    public String simplePojo(User user){
        System.out.println(user);
        return "OK";
    }
}

这里需要指出,当请求参数名中有和实体类属性名不一致的时候,不会报错,而是会自动将没有对应上的实体对象的参数赋值为默认值

1.3.2 复杂实体对象

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

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

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

定义POJO实体类:

  • Address实体类
java 复制代码
package com.yongzhi.springbootwebreqresp.pojo;

public class Address {
    private String province;
    private String city;

    public Address() {
    }

    public Address(String province, String city) {
        this.province = province;
        this.city = city;
    }

    /**
     * 获取
     * @return province
     */
    public String getProvince() {
        return province;
    }

    /**
     * 设置
     * @param province
     */
    public void setProvince(String province) {
        this.province = province;
    }

    /**
     * 获取
     * @return city
     */
    public String getCity() {
        return city;
    }

    /**
     * 设置
     * @param city
     */
    public void setCity(String city) {
        this.city = city;
    }

    public String toString() {
        return "Address{province = " + province + ", city = " + city + "}";
    }
}
  • User实体类
java 复制代码
package com.yongzhi.springbootwebreqresp.pojo;

public class User {
    private String name;
    private int age;

    private Address address;


    public User() {
    }

    public User(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     * @return address
     */
    public Address getAddress() {
        return address;
    }

    /**
     * 设置
     * @param address
     */
    public void setAddress(Address address) {
        this.address = address;
    }

    public String toString() {
        return "User{name = " + name + ", age = " + age + ", address = " + address + "}";
    }
}

Postman测试:

1.4 数组集合参数

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

这多个值也是一个个提交的,而后端接收这种数据有两种方式:

  1. 数组
  2. 集合

1.4.1 数组

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

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/arrayParam")
    public String arrayParam(String[] hobby){
        System.out.println(Arrays.toString(hobby));
        return "OK";
    }
}

Postman测试:

在前端请求时,有两种传递形式:

方式一: xxxxxxxxxx?hobby=game&hobby=java

方式二:xxxxxxxxxxxxx?hobby=game,java

1.4.2 集合

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

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

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/listParam")
    public String listParam(@RequestParam List<String> hobby){
        System.out.println(hobby);
        return "OK";
    }
}

1.5 日期参数

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

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

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

Controller方法:

java 复制代码
@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测试:

1.6 JSON参数

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

学习JSON格式参数,主要从以下两个方面着手:

  1. Postman在发送请求时,如何传递json格式的请求参数

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

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

@RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)

实体类:Address

java 复制代码
public class Address {
    private String province;
    private String city;
    
	//省略GET , SET 方法
}

实体类:User

java 复制代码
public class User {
    private String name;
    private Integer age;
    private Address address;
    
    //省略GET , SET 方法
}    

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //JSON参数
    @RequestMapping("/jsonParam")
    public String jsonParam(@RequestBody User user){
        System.out.println(user);
        return "OK";
    }
}

1.7 路径参数

在现在的开发中,经常还会直接在请求的URL中传递参数。例如:

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

上述的这种传递请求参数的形式呢,我们称之为:路径参数

路径参数:

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

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}")
    public String pathParam(@PathVariable Integer id){
        System.out.println(id);
        return "OK";
    }
}

Postman测试:

传递多个路径参数:

Postman:

Controller方法:

java 复制代码
@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}/{name}")
    public String pathParam2(@PathVariable Integer id, @PathVariable String name){
        System.out.println(id+ " : " +name);
        return "OK";
    }
}

2 响应

2.1 @ResponseBody注解

  • 类型:方法注解、类注解
  • 位置:书写在Controller方法上或类上
  • 作用:将方法返回值直接响应给浏览器
    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

需要指出的是,在上面的代码中,我们并没有添加@ResponseBody注解,但依然能够将结果响应给浏览器,这是因为我们在类上添加的@RestController注解,是一个组合注解:

  • @RestController = @ResponseBody + @Controller

@RestController源码:

java 复制代码
@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME)  //元注解
@Documented    //元注解
@Controller   
@ResponseBody 
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

故我们在类上添加了@RestController注解就相当于添加了@ResposeBody注解

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

2.2 统一响应结果

对于前端开发人员来讲,如果拿到响应数据,但是没有统一的规范,那么就需要针对不同的响应数据就要使用不同的响应方式,这就会造成开发成本高,项目不方便管理,维护起来也会很困难,所以我们就需要统一响应结果,这样对于前端人员来说,只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。

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

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

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

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

3 分层解耦

3.1 三层架构

3.1.1 介绍

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

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

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

一般情况下,我们会将业务分成三个部分:

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

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

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

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

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

这样,按照三层架构的思想,我们倘若要对业务逻辑(Service层)进行变更,也不会影响到Controller层和Dao层,这使得程序的扩展性和业务性变得更好了

总之,三层架构的好处可以归纳为:

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

3.2 分层解耦

上面我们说的是分层的思想,接下来说一下解耦的思想,所谓解耦,就是接触耦合的意思

3.2.1 耦合问题

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

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

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

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

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

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

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

3.2.2 解耦思路

  • 我们在使用一个类里面的方法时,我们首先需要创建对应的对象然后调用对应的方法,而我们要解耦,首先就不能在其他类里面使用new对象
  • 但此时又会面临一个问题,我们如果不new出对应的对象,便不能在其他类里面使用对应的方法了,那该怎么办呢?
    • 我们的解决思路是:

      • 提供一个容器,容器中存储一些对象
      • controller程序从容器中获取对应类型的对象

要实现上述解耦操作,就涉及到Spring中的两个核心概念:

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

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

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

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

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

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

3.3 IOC&DI

3.3.1 bean对象声明

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

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

注意事项:

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

3.3.2 IOC详解

当我们要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component

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

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

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

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

当我们使用注解声明了一个bean后,这个bean对象不一定会生效,这是因为使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

需要指出:

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

@SpringBootApplication源码

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "nameGenerator"
    )
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

所以为了避免不必要的麻烦,一般推荐的做法是:

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

3.3.3 DI详解

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

我们可以使用@Autowired这个注解,完成依赖注入的操作,而这个Autowired翻译过来叫:自动装配。

@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作),那么问题来了,当IOC容器中,存在多个相同类型的bean对象时,进行组件扫描的时候就会区分不了该用那个bean对象,从而报错。

为了解决上述问题,Spring提供了三种解决方案:

  • @Primary

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

  • @Qualifier

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

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

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

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

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入
相关推荐
m0_748256143 小时前
SpringBoot
java·spring boot·后端
Mr.朱鹏4 小时前
针对Feign客户端请求体参数处理问题
java·jvm·spring boot·spring·spring cloud·maven·intellij-idea
多想和从前一样4 小时前
Django 创建表时 “__str__ ”方法的使用
后端·python·django
涛粒子6 小时前
Spring Bean 生命周期的执行流程
java·后端·spring
钝挫力PROGRAMER6 小时前
SpringBoot中Mybatis记录执行sql日志
spring boot·sql·mybatis
赵琳琅6 小时前
Java语言的云计算
开发语言·后端·golang
赵琳琅6 小时前
MDX语言的安全开发
开发语言·后端·golang
林林总肿7 小时前
Mybatis后端数据库查询多对多查询解决方案
数据库·spring boot·mybatis
夏梓蕙8 小时前
Elixir语言的软件开发工具
开发语言·后端·golang
夏梓蕙8 小时前
R语言的Web开发
开发语言·后端·golang