Servlet

1.编写一个简单的Servlet程序

1.1 创建项目

Maven是JAVA中一个常用的"构建工具".

一个程序,编写过程中,往往需要涉及到一些第三方库的依赖,另外还需要对这个写好的程序进行打包部署.

Maven存在的意义,就是为了能够方便的进行依赖管理和打包.

1.2 引入依赖

当前的代码要使用Servlet开发,而Servlet并不是java标准库自带的,就需要让maven能够把Servlet的依赖给获取出来.

在maven中央仓库中找到依赖.

java 复制代码
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

手动创建一个<dependencies>标签,把刚才的坐标复制到这个标签里.

此时IDEA就会自动的通过maven从中央仓库来下载这里的依赖.

1.3 手动创建一些必要的目录文件

此处的目录结构,目录名字,都是固定的.

web.xml就是告诉tomcat,现在这个目录里的东西就是一个webapp,要把加载起来.

当然,web.xml里,需要写一些固定的内容.

java 复制代码
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

1.4 编写代码

这里简单写一个Hello World

  • 这个doGet方法不需要手动调用,doGet本质上也是一个"回调函数".

把这个方法写好之后,就会交给Tomcat,Tomcat在收到一个合适的GET请求的时候,就会自动调用doGet.

调用doGet的时候,tomcat就会解析这次的HTTP请求.生成一个HTTPServletRequest对象.(这个对象里的属性什么的都是和HTTP协议格式匹配的)

同时,Tomcat也会构造出一个空的HTTPServletResponse对象(这个空不是null,而是一个new好的,但是没有初始化属性的对象,把这个resp对象也会传递到doGet里面)

doGet要做的事情,就是根据这次请求,计算出响应.

doGet里的代码,就是根据rep里不同的参数的细节,生成一个具体的resp对象(往空对象里设置属性)

tomcat就会根据这个响应对象,转换成符合HTTP协议的响应报文,返回给浏览器了.

总之,doGet做的事,就是根据请求计算出响应.


一个Servlet程序里,可能有多个Servlet类的.

这些Servlet类,就需要在不同的情况下被执行到了.

当请求的路径中带有hello的时候,才能执行到这个HelloServlet的代码.

不同的Servlet类,就可以关联到不同的路径.

问:

这个代码写完了吗,不需要一个main方法嘛?

  • Servlet程序,不需要main方法!!!
  • 一个程序,是需要main方法,作为入口.
  • 实际上,我们写的这个代码,并不是独立的程序,而是放到Tomcat上执行的.
  • main方法其实是在tomcat里的.
  • 我们写的这些doGet之类的,都是让Tomcat来调用的.

1.5 打包程序

我们的程序是放在Tomcat上执行的,就需要对程序进行打包.

打成一个Tomcat能识别的包的格式,此时代码才会被Tomcat给加载起来.

打包,也是借助maven完成的.

看到BUILD SUCCESS即为打包成功.

此时,就会出现下述内容.

在maven中,默认打包生成的是jar包,但是tomcat需要的是war包!

此时就需要修改pom.xml,添加一个<packaging>

当然,也可以给打包命名

下面重新打包,就可以把这个war包放到tomcat里了.

1.6 部署

把写好的war包,放到Tomcat上.

具体就是,把这个war包拷贝到Tomcat的webapps的目录中.

启动Tomcat即可.

1.7 验证程序,是否能够正常工作

通过浏览器发起HTTP Get请求,触发刚才的Servle代码.

通过第一级路径,确定一个webapp

通过第二级路径,确定哪个Servlet

通过方法,确定执行Servlet中的哪个方法

2. Smart Tomcat插件

观察上述程序,如果要修改代码,就得重复步骤5,6,7.比较麻烦

这个时候就可以使用Smart Tomcat插件,让IDEA和Tomcat集成起来.

第一次使用Smart Tomcat需要简单配置

点击这里,就可以运行了.


  • 这是怎么回事呢?

因为我们前面启动了Tomcat,之前的Tomcat已经占用了8080,一个端口号只能被一个进程绑定.

把之前的Tomcat关闭,再启动:

此时,启动成功.

3. 一些访问出错

3.1 404

浏览器要访问的资源,在服务器上不存在!!!

  • ①检查你请求的路径,和你服务器这边的配置,是否一致.
  • ②确认你的webapp是否被正确加载.

Smart Tomcat由于只是加载这一个webapp,如果加载失败,就会直接启动失败.

拷贝war的方式,Tomcat要加载多个webapp,如果加载失败,只会有日志.(观察是否有"部署成功")

如果web.xml没有/目录错了/内容错了/名字拼写错了,都可能引起加载失败!!!

3.2 405

①写的doXX方法,和请求发起的方法,是不匹配的.

浏览器发起GET请求,服务器代码写的是doPost

在浏览器地址栏输入URL,发起的是get请求,但是服务器写的是doPost.


②发的是Get请求,服务器写的也是doGet,但是没有把supe.doGet给删了.

3.3 500

服务器内部错误,代码中抛出异常了.

3.4 空白页面

往往是没有执行getWriter.write方法.

3.5 无法访问此网站

这种情况,要么就是Tomcat服务器没有正确运行.

要么就是ip或端口号编写的不对.

4.Servlet API

API就是一组类和方法.

Servlet中的类很多,这里详解其中三个

4.1 HTTPServlet

我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法,让Tomcat去调用到这里的逻辑.

4.1.1 核心方法

|------------------------------|---------------------------------|
| 方法名称 | 方法名称 |
| init | 在 HttpServlet 实例化之后被调用一次 |
| destory | 在 HttpServlet 实例不再使用的时候调用一次 |
| service | 收到 HTTP 请求的时候调用 |
| doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
| doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
| doPut/doDelete/doOptions/... | 收到其他请求的时候调用(由 service 方法调用) |

①init方法:

webapp被加载的时候,执行init,使用这个方法,进行一些初始化操作.

②destory方法:

webapp在被销毁的时候(Tomcat结束)执行destory,使用这个方法进行一些收尾工作.

当然,这个方法不保证能够被调用到.

1.通过8005端口,给Tomcat发起特殊的请求,Tomcat就关闭了.(能够执行destory)

2.直接杀死Tomcat进程.(无法执行到destory了)

③service方法:

每次收到请求,都会执行service,处理每个请求.

往往使用doXX方法替代service.


[面试题]谈谈Servlet的生命周期:

生命周期,更严格的理解"什么阶段,做什么事情".

就像人的幼儿,少年,青年,中年,老年,每个时期要做的事情是不一样的.

1)webapp刚被加载的时候,调用Servlet的init方法.

2)每次收到请求的时候,调用service方法.

3)webapp要结束的时候,调用destroy方法.


4.1.2 示例一:处理请求

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/method")
public class MethodServlet extends HelloServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("doGet");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("doPost");
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("doPut");
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("doDelete");
    }
}

针对上述的这三个方法,浏览器只能比较方便的构get请求,不太方便构造其它的.使用ajax或postman.

4.2 HTTPServletRequest

HTTPServletRequest是和HTTP请求数据,是匹配的.

4.2.1 核心方法

|--------------------------------------------|----------------------------------------------|
| 方法 | 描述 |
| String getProtocol() | 返回请求协议的名称和版本。 |
| String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
| String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请 求的 URL 的一部分。 |
| String getContextPath() | 返回指示请求上下文的请求 URI 部分。 |
| String getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串。 |
| Enumeration getParameterNames() | 返回一个 String 对象的枚举,包含在该请求中包含的参数的名 称。 |
| String getParameter(String name) | 以字符串形式返回请求参数的值,或者如果参数不存在则返回 null。 |
| String[] getParameterValues(String name) | 返回一个字符串对象的数组,包含所有给定的请求参数的值,如 果参数不存在则返回 null。 |
| Enumeration getHeaderNames() | 返回一个枚举,包含在该请求中包含的所有的头名。 |
| String getHeader(String name) | 以字符串形式返回指定的请求头的值。 |
| String getCharacterEncoding() | 返回请求主体中使用的字符编码的名称。 |
| String getContentType() | 返回请求主体的 MIME 类型,如果不知道类型则返回 null。 |
| int getContentLength() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长 度未知则返回 -1。 |
| InputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象. |

①String getRequestURI()方法:

URL唯一资源定位符,描述了网络上的一个资源.

URI唯一资源标识符.

URL也可以理解成URI的一种实现方式.

②Enumeration getParameterNames()和String getParameter(String name)方法:

可以通过一些方式,给服务器传递自定义数据.

1.query string

2.body(通过post form表单的形式提交的请求的话,此时body也是键值对格式)

query string本身就是键值对结构的数据,Tomcat收到这个请求之后,就会把这个query string解析成Map,使用getParameter就可以根据key获取到value.

③Enumeration getHeaderNames()和String getHeader(String name)方法:

获取到请求头里的键值对.

Tomcat收到请求之后也会把请求头解析长Map.

④InputStream getInputStream()方法:

读取这个流对象就能得到body的内容.

Ajax也可以把提交的数据放到query string中,使用getParameter获取.

如果使用ajax POST提交json格式的数据(或者其它非form表单的格式),就需要getInputStream来获取了.


4.2.2 示例一:打印请求信息

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

@WebServlet("/request")
public class RequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这个操作是必要的,显示告诉浏览器,你拿到的数据是html
        resp.setContentType("text/html");
        //调用req的各个方法,把得到的结果汇总到一个字符串中,统一返回到页面上.
        StringBuilder respBody = new StringBuilder();


        //下列内容是在浏览器上按照html的方式来展示的,此时\n在html中并不是换行.
        //而使用<br>标签来表示换行
        respBody.append(req.getProtocol());
        respBody.append("<br>");
        respBody.append(req.getMethod());
        respBody.append("<br>");
        respBody.append(req.getRequestURI());
        respBody.append("<br>");
        respBody.append(req.getQueryString());
        respBody.append("<br>");

        //拼接header
        Enumeration<String> headers = req.getHeaderNames();//枚举
        //迭代器遍历枚举里面的每个部分
        while (headers.hasMoreElements()){
            String header = headers.nextElement();//取回
            respBody.append(header);
            respBody.append(header + ": " + req.getHeader(header));//header的key和value
            respBody.append("<br>");
        }

        //返回结果
        resp.getWriter().write(respBody.toString());
    }
}

如何获取到query string和body的数据?

4.2.3 示例二:获取 GET 请求中的参数

获取query string:

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/parameter")
public class ParameterServlet extends HttpServlet {
    //约定,客户端使用query string传递数据
    //query string形如: username=zhangshan&password=123
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        System.out.println("username= "+ username);
        System.out.println("password= "+ password);
        resp.getWriter().write("ok");
    }
}

4.2.4 示例三: 获取 POST 请求中的参数

4.2.4.1 获取body(考虑form表单的格式)

form表单:和query string格式意义,也是键值对.

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/parameter2")
public class Parameter2Servlet extends HttpServlet {
    //预期让客户端发送一个POST请求,同时使用form格式的数据,在body中把数据传送过来
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        System.out.println("username= "+ username);
        System.out.println("password= "+ password);
        resp.getWriter().write("ok");
    }
}
4.2.4.2 获取body(考虑json格式)

这种格式在开发中非常常见.

Servlet自身不能对json格式的数据进行解析,需要引入第三方库:

java 复制代码
 <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>
  • 那么如何使用ObjectMapper?

Map也叫做映射表.

把一个对象映射到JSON字符串,也可以把JSON字符串映射到对象.

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


class User{
    public String username;
    public String password;
}

@WebServlet("/json")
public class JsonServlet extends HttpServlet {

    /**
     此处约定客户端body按照json格式来进行传输
     * {
     *     username:"zhangshan"
     *     password:"123"
     * }
     */

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            ObjectMapper objectMapper = new ObjectMapper();
            User user = objectMapper.readValue(req.getInputStream(),User.class);//从字符串到对象
            System.out.println("username= " + user.username +", password= "+user.password);
            //也可以java对象转成json字符串
            String userString = objectMapper.writeValueAsString(user);
            System.out.println("userString: "+userString);
            resp.getWriter().write("ok");
    }

}

这个方法有很多版本,作用就是把json字符串给解析成JAVA对象.

其中这里第一个参数,是一个流对象,也就表示json从哪里来读.

请求的body是通过getlnputStream得到流对象,进一步读出来的.

请求的第二个参数,则是指定的类型.当前你这边得到的json字符串,需要转成什么样的一个java对象,需要指定一下对象的类型.


就可以定义一个类,让这个类里的属性的名字和类型都和json字符串匹配.

这里必须先知道json的结构是什么,才能够根据json结构,构造出User对象.

readValue就把req的body中的字符串读取并解析了,然后构造成了User对象,User对象中的属性,就是前面json中所体现的内容.


  • 在这里写json格式的时候,key也加上了",但是之前在注释里没写?

在ison格式中,key一定是字符串类型,所以原则上,key不写"也是完全可以的.

但是有些库/程序,检查更加严格,就必须强制写".

js的ajax方式构造json,此时key是没有引号的.

但是使用postman构造的json,此时key就需要引号.


  • 此处的public能不能写成private?

不能,会出现500.

本身,jackson会通过反射的方式,把User类里包含的public的属性给获取到.

此时,就可以根据反射这里得到的"属性名字",去json解析出来的键值对中进行匹配.

如果匹配到了,就把value设置到刚才得到的属性中.

由于把username改成了private,而Jackson并不会直接针对private属性进行扫描,username就不认识了.

当然,提供对应的getter和setter方法就可以写成private

[重点理解]jackson的readValue工作过程:

  • 先把json字符串解析成键值对,放到Map中.
  • 再根据参数填入的类对象,通过反射API就可以知道,这个类里面有什么属性,每个属性的名字和类型.
  • 一次把这里的每个属性都取出来,通过属性名字查询上述的Map,把得到的值,赋给这个类的属性.

4.3 HTTPServletResponse

HTTPServletResponse同样也是和HTTP响应数据,是匹配的.

针对状态码,各种header,body这些属性,就可以进行"设置".

  • 请求对象,我们拿到之后的目的,是为了获取里面的属性(读).
  • 响应对象, 我们拿到之后的目的,是为了设置里面的属性(写).

对于doXX这样的方法来说,本身要做的事情就是"根据请求计算响应".

  • 请求对象,是Tomcat收到请求之后,对HTTP协议解析得到的对象.
  • 响应对象,是Tomcat创建的空的对象,我们在代码中把响应对象的属性设置好.(响应对象,相当于是一个输出型参数)

4.3.1 核心方法

|-------------------------------------------|--------------------------------------------------------|
| 方法 | 描述 |
| void setStatus(int sc) | 为该响应设置状态码。 |
| void setHeader(String name, String value) | 设置一个带有给定的名称和值的 header. 如果 name 已经存在, 则覆盖旧的值. |
| void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header. 如果 name 已经存在, 不覆盖旧的值, 并列添加新的键值对 |
| void setContentType(String type) | 设置被发送到客户端的响应的内容类型。 |
| void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如, UTF-8。 |
| void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端。 |
| PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
| OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据. |

①void setCharacterEncoding(String charset)方法:

这里是告诉浏览器,要按照什么样的字符集来解析响应的body.如果不去描述清楚,可能浏览器展示的内容就会乱码.

②void addHeader(String name, String value)方法:

使用addHeader,header中可能出现,key相同的两个键值对.

一般来说,约定键值对中的key是唯一的,但实践中,有些情况,确实也会需要key不唯一.

③void sendRedirect(String location)方法:

这个是特殊的方法,用来设置"重定向"响应.


4.3.2 示例一:设置状态码

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(200);
        //resp.sendError(404);
    }
}

其实可以在返回状态码的同时,给body写入数据,就可以得到一些"个性化的错误页面".

resp.sendError(404):


4.3.3 示例二:重定向

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //让页面被重定向到 搜狗主页
        //resp.setStatus(302);
        //重定向响应,一定腰带有Location属性,
        //resp.setHeader("Location","http://www.sogou.com");
        resp.sendRedirect("http://www.sogou.com");
    }
}

4.3.4 示例三:自动刷新

可以使用setHeader设置任意的响应报头.

通过refresh属性,设置浏览器自动刷新.

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@WebServlet("/refresh")
public class RefreshServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("refresh","2");
        //返回系统时间,方便再次观察
        resp.getWriter().write("time: "+ System.currentTimeMillis());
    }
}

设置这个属性之后,浏览器就会每隔两秒自动刷新一次!!!

但这里的刷新的间隔也不是精准的2000ms,会比2000稍微多点.

必将,浏览器发起请求,服务器响应,知道页面被解析出来,都是需要消耗一定的时间的.


4.3.5 示例四:编码方式匹配

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/body")
public class BodyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //让服务器返回一个html数据
        resp.getWriter().write("<div>你好<div>");
    }
}

  • 这是怎么回事?

浏览器,默认会跟随系统的编码.

windows简体中文版,默认的编码是gbk.

一般在IDEA里面直接写一个中文字符串,就是utf8的编码.

拿着utf8的数据,浏览器按照gbk的方式来解析,势必就会出现乱码!!!

解决乱码的原则,就是编码方式匹配.

java 复制代码
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/body")
public class BodyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置响应的字符集
        resp.setContentType("text/html;charset=utf8");
        //让服务器返回一个html数据
        resp.getWriter().write("<div>你好<div>");
    }
}

字符集,其实是ContentType的一部分.


这里要注意,给resp设置属性的时候,必须要注意顺序!!

先设置header,后设置body.

一旦开始设置body,就相当于header和status都定型了,已经来不及修改了.

相关推荐
哎呦没18 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥1 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程2 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
郭二哈3 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃3 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23073 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c3 小时前
幂等性接口实现
java·rpc
代码之光_19803 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端