Request和Response相关介绍

Request 和 Response 是什么?

  • Request(请求对象):用来接收浏览器发过来的数据。
  • Response(响应对象):用来把服务器处理后的结果返回给浏览器。


1. request 的作用(获取请求数据)

  • 当你在浏览器访问网页或提交表单时,浏览器会把你的请求(比如你输入的账号、密码等)通过HTTP协议发给服务器(Tomcat)。
  • 这些数据会被Tomcat解析,然后封装到request对象里
  • 在Servlet代码里,你可以通过request对象获取到这些数据,比如获取表单里的用户名、密码等。
  • 拿到数据后,服务器就可以根据这些信息做后续的业务处理,比如登录验证。


2. response 的作用(设置响应数据)

  • 服务器处理完业务后,需要把结果返回给浏览器,比如返回一个网页、提示信息等。
  • 这些返回的数据会被封装到response对象里
  • Tomcat会把response对象里的内容解析出来,按照HTTP协议的格式发回给浏览器。
  • 浏览器收到后,就会把内容展示出来,比如显示网页、弹出提示等。


通过一个案例来初步体验下request和response对象的使用。

写一个表单,请求方式为GET,当我们输入不同的username,并点击提交时,界面上就会出现username,欢迎访问

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<form action="http://localhost:8080/web_demo_war_exploded/demo" method="get">用户名:<input type="text" name="username"><input type="submit" value="提交">
</form>
</body>
</html>

启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容: 

import javax.servlet.*;
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(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String name = req.getParameter("username");//避免中文乱码问题resp.setHeader("content-type","text/html;charset=utf-8");resp.getWriter().write("<h1>"+name+",欢迎访问<h1>");}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("post...");}
}

总结一句话

  • request:用来接收浏览器发来的数据。
  • response:用来把服务器的处理结果返回给浏览器。

Request继承体系

一、Request继承体系是什么?

在Java Web开发中,Request对象用来封装浏览器发来的请求数据。

但这个Request对象其实有一套“继承体系”,也就是有父接口、子接口和实现类。

1. ServletRequest(接口)

  • Java官方提供的最基础的请求对象接口,定义了所有请求对象都应该有的方法。
  • 就像“交通工具”这个大类,规定了所有交通工具都应该有“启动、停止”等功能。

2. HttpServletRequest(接口)

  • 继承自ServletRequest,专门针对HTTP协议的请求对象接口,增加了很多和HTTP相关的方法(如获取请求头、参数等)。
  • 就像“汽车”是“交通工具”的一种,除了启动、停止,还能“开空调、开车窗”等。

3. RequestFacade(实现类)

  • 这是Tomcat服务器自己写的一个实现类,真正实现了HttpServletRequest接口的所有方法。
  • 就像“宝马汽车”是“汽车”的一种,宝马公司造出来的具体汽车。

二、Tomcat处理请求的过程

  1. 浏览器发请求(比如你在Chrome里访问一个网站)。
  2. Tomcat服务器接收到请求,会先解析请求数据。
  3. Tomcat用RequestFacade类,把请求数据封装成一个Request对象。
  4. Tomcat把Request对象传给Servlet的service方法,让你的Java代码可以方便地获取请求里的各种信息。

三、生活中的实际例子

例子1:快递包裹

  • ServletRequest:快递包裹的“标准接口”,规定所有快递包裹都要有“收件人、寄件人、内容物”等信息。
  • HttpServletRequest:专门针对“顺丰快递”的包裹,除了基本信息,还多了“顺丰单号、保价服务”等特殊功能。
  • RequestFacade:顺丰公司实际生产出来的某个快递包裹,里面装着你的快递。

例子2:公司员工

  • ServletRequest:公司规定的“员工”标准(必须有姓名、工号等)。
  • HttpServletRequest:技术部员工,除了基本信息,还要有“技术等级、编程语言”等。
  • RequestFacade:张三,技术部的一个具体员工。

ServletRequest和HttpServletRequest是继承关系,并且两个都是接口,接口是无法创建对象,那么方法里的HttpServletRequest参数是从哪儿来的呢?

//接口无法创建对象,那么这个参数是哪儿来的呢
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String name = req.getParameter("username");resp.setHeader("content-type","text/html;charset=utf-8");resp.getWriter().write("<h1>"+name+",欢迎访问<h1>");
}@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("post...");
}

一、接口不能创建对象,参数对象从哪来的?

  • ServletRequest 和 HttpServletRequest 都是接口,接口是不能直接用 new 创建对象的。
  • 但是在 Servlet 的 doGet、doPost 方法里,参数却是 HttpServletRequest req,这个对象是谁创建的呢?

二、Request对象的创建过程

  1. Tomcat 服务器收到浏览器请求后,会先解析请求数据。
  2. Tomcat 会用 RequestFacade 这个类,来实现 HttpServletRequest 接口,把请求数据封装成一个对象。
  3. Tomcat 再把这个对象传给你的 Servlet 的 doGet/doPost 方法,你就能直接用 req.getParameter("username") 这样的方法获取请求参数了。

简单说:你用的 req 对象,其实是 Tomcat 帮你创建好的 RequestFacade 对象,只不过它的类型是 HttpServletRequest。

三、生活中的实际例子

例子1:快递公司和快递单

  • 接口(HttpServletRequest):就像快递公司规定的“快递单标准”,规定了快递单必须有收件人、寄件人、地址等信息。
  • RequestFacade:就像顺丰公司实际印出来的快递单,完全符合标准,但是顺丰自己造的。
  • 你(Servlet):只管用快递单(req对象)填写和查信息,不用关心快递单是谁印的、怎么印的。

例子2:公司员工和岗位说明书

  • 接口:公司规定的“岗位说明书”,比如“程序员”要会写代码、会调试。
  • 实现类:张三是公司招聘的程序员,完全符合岗位说明书的要求。
  • 你(项目经理):只管让张三干活(调用方法),不用管张三是怎么被招聘进来的

四、代码中的体现

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {// req其实是Tomcat创建的RequestFacade对象String name = req.getParameter("username");// ...
}
import javax.servlet.*;
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(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println(req);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("post...");}
}

启动服务器访问页面,控制台输出org.apache.catalina.connector.RequestFacade@36bf693c 

五、总结

  • ServletRequest:最基础的请求接口,规定了所有请求对象的基本功能。
  • HttpServletRequest:专门针对HTTP协议的请求接口,功能更丰富。
  • RequestFacade:Tomcat实现的具体类,真正用来封装和传递请求数据。

Tomcat会用RequestFacade把浏览器的请求数据封装好,然后传给你的Servlet代码,让你可以方便地获取和处理请求信息。

Request获取请求数据

请求行包含三块内容,分别是请求方式请求资源路径HTTP协议及版本,例如
GET /tomcat_demo_war/index.html?username=suger1201&password=dsaasd HTTP/1.1

对于这三部分内容,request对象都提供了对应的API方法来获取,具体如下:

  • 获取请求方式: GET
    String getMethod()
  • 获取虚拟目录(项目访问路径): /request-demo
    String getContextPath()
  • 获取URI(统一资源标识符): /request-demo/req1
    String getRequestURI()

介绍完上述方法后,咱们通过代码把上述方法都使用下:

import javax.servlet.*;
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(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String method = req.getMethod();System.out.println("请求方式:" + method);String contextPath = req.getContextPath();System.out.println("项目访问路径:" + contextPath);StringBuffer requestURL = req.getRequestURL();System.out.println("URL:" + requestURL);String requestURI = req.getRequestURI();System.out.println("URI:" + requestURI);String queryString = req.getQueryString();System.out.println("请求参数:" + queryString);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("post...");}
}

这里的请求方式是GET

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<form action="/web_demo_war_exploded/demo" method="get">用户名:<input type="text" name="username"><input type="submit" value="提交">
</form>
</body>
</html>

启动服务器,并向表单中随便输入一个username,然后点击提交,控制台输出如下
请求方式:GET
项目访问路径:/web_demo_war_exploded
URL:http://localhost:8080/web_demo_war_exploded/demo
URI:/web_demo_war_exploded/demo
请求参数:username=Cyderpunk2077%40gmail.com

获取请求头数据

对于请求头的数据,格式为key: value
所以根据请求头名称获取对应值的方法为

String getHeader(String name) 

接下来,在代码中如果想要获取客户端浏览器的版本信息,则可以使用

import javax.servlet.*;
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(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String agent = req.getHeader("user-agent");System.out.println(agent);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("post...");}
}

启动服务器,控制台输出如下
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36

获取请求体数据 

浏览器在发送GET请求的时候是没有请求体的,所以需要把请求方式变更为POST

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<form action="/web_demo_war_exploded/demo" method="post">用户名:<input type="text" name="username"><input type="submit" value="提交">
</form>
</body>
</html>

对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:

  • 获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
    ServletInputStream getInputStream()
  • 获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
    BufferedReader getReader()

下面我们在Servlet的doPost方法中获取数据

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;@WebServlet(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("get??");}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//由于获取的是纯文本数据,所以这里用的getReader()BufferedReader bufferedReader = req.getReader();String line = bufferedReader.readLine();System.out.println(line);}
}

 form表单提交数据

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<form action="http://localhost:8080/request-demo/demo" method="post">用户名:<input type="text" name="username"><input type="submit" value="提交"></form>
</body>
</html>
BufferedReader流是通过request对象来获取的,当请求完成后request对象就会被销毁,request对象被销毁后,BufferedReader流就会自动关闭,所以此处就不需要手动关闭流了。

就像你收到一封信(POST请求),你用 getReader() 就是把信封拆开,直接读里面的信纸内容(请求体),而不是看信封上的地址(请求头或参数)。 

启动服务器,访问 http://localhost:8080/requset-demo/index.html ,填入username并提交,在控制台就可以看到前端发送的请求数据了

小结

HTTP请求数据中包含了请求行请求头请求体,针对这三部分内容,Request对象都提供了对应的API方法来获取对应的值:

  • 请求行
    • getMethod()获取请求方式
    • getContextPath()获取项目访问路径
    • getRequestURL()获取请求URL
    • getRequestURI()获取请求URI
    • getQueryString()获取GET请求方式的请求参数
  • 请求头
    • getHeader(String name)根据请求头名称获取其对应的值
  • 请求体
    • 注意: 浏览器发送的POST请求才有请求体
    • 如果是纯文本数据:getReader()
    • 如果是字节数据如文件数据:getInputStream()

获取请求参数的通用方式

请求参数获取方式

  • GET方式:
    String getQueryString()
  • POST方式:
    BufferedReader getReader();

思考:
GET请求方式和POST请求方式区别主要在于获取请求参数的方式不一样,是否可以提供一种统一获取请求参数的方式,从而统一doGet和doPost方法内的代码? 

解决方式一:
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;@WebServlet(urlPatterns = "/demo")
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//获取请求方式String method = req.getMethod();String param = "";//根据请求方式来获取请求参数if ("GET".equals(method)) {param = req.getQueryString();} else if ("POST".equals(method)) {BufferedReader bufferedReader = req.getReader();param = bufferedReader.readLine();}//将请求参数进行打印控制台System.out.println(param);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doGet(req, resp);}
}

代码中的一些疑问:

1. "GET".equals(method) 是什么意思?
  • method 是一个字符串变量,保存了请求方式,比如 "GET" 或 "POST"。
  • "GET".equals(method) 的意思是:判断 method 变量的值是不是 "GET"。

2. 为什么不写成 method.equals("GET")?
  • 其实 method.equals("GET") 也可以用,功能是一样的。
  • 但如果 method 变量是 null,method.equals("GET") 会报空指针异常(NullPointerException)。
  • 而 "GET".equals(method) 就算 method 是 null,也不会报错,只会返回 false。

所以这种写法更安全!


3. 还能怎么用?

这种写法适用于任何字符串比较,比如:

if ("POST".equals(method)) { ... }
if ("admin".equals(role)) { ... }
if ("zhangsan".equals(username)) { ... }
4. 生活中的例子

就像你问别人“你是不是张三?”

  • 你直接问“张三.equals(你)”,不管对方是谁(哪怕没人),你都不会出错。
  • 但如果你问“你.equals(张三)”,对方要是没人(null),你就会出错。
5. 总结
  • "GET".equals(method) 是安全的字符串比较写法,防止空指针异常。
  • 这种写法在Java开发中非常常见,推荐你以后都这样写!

使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值,这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用

解决方式二:

request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可,在request的方法中都实现了哪些操作呢?

  1. 根据不同的请求方式获取请求参数,例如:username=zhangsan&password=asd123&hobby=1&hobby=2
  2. 把获取到的内容进行分割,username=zhangsan&password=asd123&hobby=1 -> username=zhangsan password=asd123 hobby=1 hobby=2 -> username zhangsan password asd123 hobby 1 hobby 2
  3. 把分割后端数据,存入到一个Map集合中,其中Map集合的泛型为<String,String[]>,因为参数的值可能是一个,也可能有多个,所以value的值的类型为String数组。

基于上述理论,request对象为我们提供了如下方法:

  • 获取所有参数Map集合
    Map<String,String[]> getParameterMap()
  • 根据名称(Key)获取参数值(Value)(数组)
    String[] getParameterValues(String name)
  • 根据名称(Key)获取参数值(Value)(单个值)
    String getParameter(String name)

接下来,我们通过案例来把上述的三个方法进行实例演示:

  1. 随便写一个表单,加上一个复选框,爱好可以多选,所以到时候的参数值就不止一个了
    <!DOCTYPE html>
    <html lang="en">
    <head><meta charset="UTF-8"><title>Title</title>
    </head>
    <body>
    <form action="/web_demo_war_exploded/demo" method="post">用户名:<input type="text" name="username"><br>密码:<input type="password" name="password"><br>爱好:<input type="checkbox" name="hobby" value="1">Apex<input type="checkbox" name="hobby" value="2">Terraria<br><input type="submit" value="提交">
    </form>
    </body>
    </html>

    getParameterMap()

    import javax.servlet.*;
    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.Map;@WebServlet(urlPatterns = "/demo")
    public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//获取所有参数的Map集合Map<String, String[]> parameterMap = req.getParameterMap();//遍历Mapfor (String key : parameterMap.keySet()) {System.out.print(key+":");String[] values = parameterMap.get(key);for (String v : values) {System.out.print(v+" ");}System.out.println();}}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doGet(req, resp);}
    }
    /*
    获取的结果如下
    username:Cyderpunk2077@gmail.com 
    password:dsaasd 
    hobby:1 2 
    */

    getParameterValues(String name)

    @WebServlet(urlPatterns = "/demo")
    public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String[] hobbies = req.getParameterValues("hobby");for (String s : hobbies) {System.out.println(s);}}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doGet(req, resp);}
    }
    /*
    将两个复选框都勾上,得到结果如下
    1
    2
    */

    getParameter(String name)

    @WebServlet(urlPatterns = "/demo")
    public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String username = req.getParameter("username");System.out.println(username);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doGet(req, resp);}
    }
    //输出结果就是你输入的username

    如果你在post请求方式下输入了中文username并提交,控制台会输出乱码
    解决方案:

  

package com.itheima.request;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("/req")
public class RequestDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String username = req.getParameter("username");
// 先用ISO-8859-1解码,再用UTF-8编码username = new String(username.getBytes("ISO-8859-1"), "UTF-8");System.out.println(username);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doGet(req,resp);}
}

遇到的问题

new String(username.getBytes("ISO-8859-1"), "UTF-8")是什么作用?

它的作用就是:把已经乱码的字符串“修正”回来,变成正常的中文。

 详细分解

  • username.getBytes("ISO-8859-1")

把“乱码”的字符串,按照ISO-8859-1编码方式转成字节数组。

这一步的意思是:把你看到的乱码,变回原始的字节数据。

  • new String(..., "UTF-8")

用UTF-8编码方式,把上一步得到的字节数组重新解码成字符串。

这一步的意思是:用正确的方式重新“翻译”这些字节,得到正常的中文。

这行代码就是“先把乱码还原成字节,再用UTF-8重新解码,得到正常中文”。

方法作用缺点
req.getParameterMap()获取所有参数的 Map(参数名→值数组 )需遍历处理
req.getParameter("username")获取单个参数的第一个值无法直接获取多值参数(如 hobby )
req.getParameterValues("hobby")获取单个参数的所有值(返回数组 )需逐个参数调用

Request请求转发

请求转发(forward):一种在服务器内部的资源跳转方式。

  1. 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
  2. 资源A处理完请求后将请求发给资源B
  3. 资源B处理完后将结果响应给浏览器
  4. 请求从资源A到资源B的过程就叫请求转发

 测试步骤

  1. 创建一个RequestDemo1类,接收/req5的请求,在doGet方法中打印这里是RequestDemo1
    import javax.servlet.*;
    import javax.servlet.http.*;
    import javax.servlet.annotation.*;
    import java.io.IOException;@WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是RequestDemo1");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }

  2. 创建一个RequestDemo2类,接收/req6的请求,在doGet方法中打印这里是RequestDemo2
    import javax.servlet.*;
    import javax.servlet.http.*;
    import javax.servlet.annotation.*;
    import java.io.IOException;@WebServlet("/RequestDemo2")
    public class RequestDemo2 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是RequestDemo2");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
  3. 在RequestDemo1的方法中使用req.getRequestDispatcher("/RequestDemo2").forward(req,resp)进行请求转发
    import javax.servlet.*;
    import javax.servlet.http.*;
    import javax.servlet.annotation.*;
    import java.io.IOException;@WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是RequestDemo1");//加上这行request.getRequestDispatcher("/RequestDemo2").forward(request, response);}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }

  4. 启动测试
    当我们访问RequestDemo1时,控制台会得到如下输出

        这里是RequestDemo1
        这里是RequestDemo2

        说明请求已经转发到了/RequestDemo2  

在转发的同时我们还可以传递数据给/RequestDemo2


request对象提供了三个方法:
  • 存储数据到request域[范围,数据是存储在request对象]中
    void setAttribute(String name,Object o);
  • 根据key获取值
    Object getAttribute(String name);
  • 根据key删除该键值对
    void removeAttribute(String name);

接着上个需求来:

  1. 在RequestDemo1的doGet方法中转发请求之前,将数据存入request域对象中.
    @WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是RequestDemo1");//存储数据request.setAttribute("msg","HELLO~");//请求转发request.getRequestDispatcher("/RequestDemo2").forward(request, response);}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
  2. 在RequestDemo2的doGet方法从request域对象中获取数据,并将数据打印到控制台.
    @WebServlet("/RequestDemo2")
    public class RequestDemo2 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是RequestDemo2");Object msg = request.getAttribute("msg");System.out.println(msg);}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
  3. 启动访问测试,得到的输出结果如下

        这里是RequestDemo1
        这里是RequestDemo2
        HELLO~

请求转发的特点
  • 浏览器地址栏路径不发生变化
    虽然后台从/这里是RequestDemo1转发到/这里是RequestDemo2,但是浏览器的地址一直是/这里是RequestDemo1,未发生变化
  • 只能转发到当前服务器的内部资源
    不能从一个服务器通过转发访问另一台服务器
  • 一次请求,可以在转发资源间使用request共享数据
    虽然后台从/RequestDemo1转发到/RequestDemo2,但是这个只有一次请求
遇到的问题
1. request.setAttribute 是什么?
  • 这是 HttpServletRequest 提供的方法,用来在一次请求的范围内存储数据。
  • 你可以把它理解为“在本次请求的背包里放东西”,后续在请求转发(forward)到其他 Servlet 或 JSP 时,可以随时取出来用。
2. 语法和用法
request.setAttribute("键", 值);
  • "msg" 是你给数据起的名字(key)。
  • "HELLO~" 是你要存的数据(value),可以是任意对象。
3. 作用和场景
  • 作用:在一次请求内,多个 Servlet/JSP 之间共享数据。
  • 常见场景:请求转发(forward)时传递数据,比如表单校验、页面跳转时传递提示信息等。
4. 取出数据的方法

在被转发的 Servlet 或 JSP 中,可以这样取出数据:

Object msg = request.getAttribute("msg");
System.out.println(msg); // 输出:HELLO~
5. 注意事项
  • request.setAttribute 存的数据只在本次请求有效,请求结束就没了。
  • 如果用的是重定向(redirect),数据不会带过去,只有请求转发(forward)才可以。
6. 总结
  • request.setAttribute("msg", "HELLO~"); 就是把数据存到本次请求的“背包”里,方便后续在同一次请求的其他地方取用。
  • 常用于Servlet/JSP 之间的数据传递。

Response对象

Reponse的继承体系和Request的继承体系也非常相似:

Response设置响应数据功能介绍

HTTP响应数据总共分为三部分内容,分别是==响应行、响应头、响应体==,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?

  1. 响应行
    响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
    对于响应头,比较常用的就是设置响应状态码:
    void setStatus(int sc);
  2. 响应头
    响应头的格式为key:value形式
    设置响应头键值对:
    void setHeader(String name,String value);
  3. 响应体
    对于响应体,是通过字符、字节输出流的方式往浏览器写,
    获取字符输出流:
    PrintWriter getWriter();

    获取字节输出流

    ServletOutputStream getOutputStream();

     

Response请求重定向 

  • 重定向的实现方式
    response.setStatus(302);
    response.setHeader("location","资源B的访问路径");
    //或
    resposne.sendRedirect("资源B的访问路径");
  • 创建一个ResponseDemo1类,接收/ResponseDemo1的请求,在doGet方法中打印这里是ResponseDemo1
    @WebServlet("/ResponseDemo1")
    public class ResponseDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是ResponseDemo1");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
  • 创建一个ResponseDemo2类,接收/ResponseDemo1的请求,在doGet方法中打印这里是ResponseDemo2
    @WebServlet("/ResponseDemo2")
    public class ResponseDemo2 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是ResponseDemo2");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
  • 在ResponseDemo1的方法中使用response.sendRedirect("/web_demo_war_exploded/RequestDemo2");
    @WebServlet("/ResponseDemo1")
    public class ResponseDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是ResponseDemo1");//重定向response.sendRedirect("/web_demo_war_exploded/RequestDemo2");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }
    
  • 启动测试,访问/ResponseDemo1,控制台得到如下输出,同时地址栏的地址也变更为/RequestDemo2

这里是RequestDemo1
这里是RequestDemo2

重定向的特点
  • 浏览器地址栏路径发送变化
    当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化
  • 可以重定向到任何位置的资源(服务内容、外部均可)
    因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。
  • 两次请求,不能在多个资源使用request共享数据
    因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据

 

简单解释重定向和请求转发的区别

 

一、重定向(Redirect)
1. 什么是重定向?

重定向就是服务器告诉浏览器“你去别的地方找”,然后浏览器自己重新发起请求。

2. 生活中的例子

场景:你去银行办业务

  1. 你走进银行A,问“我要办信用卡”。
  1. 银行A的工作人员说:“我们这里不办信用卡,请您去银行B,地址是XX路XX号。”
  1. 你走出银行A,自己走到银行B。
  1. 在银行B办完信用卡。
3. 重定向的特点
  • 浏览器地址栏会变:因为你去了新的地方,地址栏显示银行B的地址。
  • 可以跳转到任何地方:银行A可以让你去银行B,也可以让你去邮局、超市等。
  • 两次请求:你去了银行A一次,又去了银行B一次。
  • 不能带信息:银行A不能直接把你的资料传给银行B,你得重新说明。
4. 代码示例 
// 重定向到另一个页面
resp.sendRedirect("/login.html");
二、请求转发(Forward)
1. 什么是请求转发?

请求转发就是服务器内部帮你转接,浏览器不知道发生了跳转。

2. 生活中的例子

场景:你打电话给客服

  1. 你打电话给客服A,问“我要投诉”。
  1. 客服A说:“这个问题我帮你转接给投诉部门,请稍等。”
  1. 客服A在内部把你的电话转给了投诉部门。
  1. 投诉部门直接和你通话,解决问题。
3. 请求转发的特点
  • 浏览器地址栏不变:因为你一直以为在和客服A通话,地址栏还是显示客服A的号码。
  • 只能转接到内部:客服A只能转给公司内部的部门,不能转给其他公司。
  • 一次请求:你只打了一次电话,只是内部转接了。
  • 可以带信息:客服A可以把你的问题、会员号等信息直接转给投诉部门。
4. 代码示例
// 请求转发到另一个Servlet
req.getRequestDispatcher("/demo2").forward(req, resp);

三、对比总结

| 方面 | 重定向 | 请求转发 |

|------|--------|----------|

| 地址栏 | 会变 | 不变 |

| 请求次数 | 两次 | 一次 |

| 跳转范围 | 任何地方 | 只能内部 |

| 数据传递 | 不能 | 可以 |

| 效率 | 较低 | 较高 |

四、什么时候用哪个?

一、什么时候用重定向?

1. 登录成功后跳转到首页

场景: 用户登录成功,需要跳转到网站首页。

为什么用重定向?

  • 登录成功后,如果用户刷新页面,不会重复提交登录信息。
  • 地址栏会变成首页地址,用户可以直接收藏或分享首页链接。

代码示例:

// 登录验证成功后
if (loginSuccess) {resp.sendRedirect("/index.html"); // 重定向到首页
}

2. 表单重复提交避免

场景: 用户提交订单后,如果刷新页面,不会重复提交订单。

为什么用重定向?

  • 用户提交订单后,重定向到订单成功页面。
  • 如果用户刷新页面,只会刷新订单成功页面,不会重复提交订单。

代码示例:

// 订单提交成功后
if (orderSuccess) {resp.sendRedirect("/order-success.html"); // 重定向到成功页面
}
二、什么时候用请求转发?

1. 页面内部模块跳转

场景: 在一个页面内部,根据不同的条件显示不同的内容。

为什么用请求转发?

  • 保持同一个URL,用户不知道内部发生了跳转。
  • 可以在不同模块间传递数据。

代码示例:

// // 用户访问 /dashboard
if (userRole.equals("admin")) {// 管理员看到管理面板req.getRequestDispatcher("/admin-dashboard.jsp").forward(req, resp);
} else if (userRole.equals("user")) {// 普通用户看到用户面板req.getRequestDispatcher("/user-dashboard.jsp").forward(req, resp);
} else {// 游客看到欢迎页面req.getRequestDispatcher("/guest-dashboard.jsp").forward(req, resp);
}

场景:根据商品类型显示不同的详情模板

// 用户访问 /product/detail?id=123
String productType = product.getType();
if (productType.equals("phone")) {// 手机显示手机专用模板req.getRequestDispatcher("/phone-detail.jsp").forward(req, resp);
} else if (productType.equals("laptop")) {// 笔记本显示笔记本专用模板req.getRequestDispatcher("/laptop-detail.jsp").forward(req, resp);
} else if (productType.equals("book")) {// 书籍显示书籍专用模板req.getRequestDispatcher("/book-detail.jsp").forward(req, resp);
}

效果: 用户访问的都是 /product/detail,但根据商品类型显示不同模板。

场景:根据订单状态显示不同的处理页面

// 用户访问 /order/status?id=456
String orderStatus = order.getStatus();
switch (orderStatus) {case "pending":// 待付款状态req.getRequestDispatcher("/order-pending.jsp").forward(req, resp);break;case "paid":// 已付款状态req.getRequestDispatcher("/order-paid.jsp").forward(req, resp);break;case "shipped":// 已发货状态req.getRequestDispatcher("/order-shipped.jsp").forward(req, resp);break;case "delivered":// 已送达状态req.getRequestDispatcher("/order-delivered.jsp").forward(req, resp);break;
}

效果: 用户访问的都是 /order/status,但根据订单状态显示不同内容。

路径问题

  • 问题1:转发的时候路径上没有加虚拟目录web_demo_war_exploded,而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?

  • 其实判断的依据很简单,只需要记住下面的规则即可:

    • 浏览器使用:需要加虚拟目录(项目访问路径)
    • 服务端使用:不需要加虚拟目录

解释为什么浏览器要加虚拟目录,服务端不加。

一、什么是虚拟目录?

虚拟目录就是项目在服务器上的访问路径,比如:

  • 你的项目叫 web_demo
  • 访问路径是 http://localhost:8080/web_demo/
  • 这里的 /web_demo/ 就是虚拟目录

二、为什么浏览器要加虚拟目录?
// 重定向
resp.sendRedirect("/web_demo/login.html");

原因:

  • 重定向是服务器告诉浏览器“你去访问这个地址”
  • 浏览器收到指令后,自己重新发起请求
  • 浏览器是外部客户端,不知道你的项目在服务器上的具体位置
  • 所以必须告诉浏览器完整的访问路径(包含虚拟目录)

生活中的例子

  • 你问路,别人告诉你“去银行”,但没说具体地址
  • 你必须知道银行的完整地址(包含街道、门牌号),才能找到
三、为什么服务端不加虚拟目录?

请求转发的情况

// 请求转发
req.getRequestDispatcher("/login.html").forward(req, resp);

原因:

  • 请求转发是服务器内部操作
  • 服务器知道你的项目在文件系统中的真实位置
  • 不需要通过外部的虚拟目录来定位资源
  • 直接使用内部路径即可

生活中的例子

  • 你在公司内部,同事说“去找张三”
  • 你知道张三在哪个办公室,不需要知道公司的门牌号
问题2:在重定向的代码中,``web_demo_war_exploded`是固定编码的,如果后期通过Tomcat插件配置了项目的访问路径,那么所有需要重定向的地方都需要重新修改,该如何优化?

在代码中动态去获取项目访问的虚拟目录,request对象中提供了getContextPath()方法

@WebServlet("/ResponseDemo1")
public class ResponseDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("这里是ResponseDemo1");//获取虚拟目录String contextPath = request.getContextPath();//把虚拟目录拼在前面response.sendRedirect(contextPath + "/RequestDemo2");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
}

Response响应字符数据

要想将字符数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();

  • 通过字符输出流写数据: writer.write(“aaa”);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个简单的字符串hello world
    @WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {PrintWriter printWriter = response.getWriter();printWriter.write("hello world");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }

    返回一串html字符串,并且能被浏览器解析

  2. 返返回一串html字符串,并且能被浏览器解析,需要注意设置响应数据的编码为utf-8
    @WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//设置响应的数据格式及数据的编码response.setContentType("text/html;charset=utf-8");PrintWriter printWriter = response.getWriter();printWriter.write("<h1>你好<h1>");}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }

Response响应字节数据

要想将字节数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();

  • 通过字节输出流写数据: outputStream.write(字节数据);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个图片文件到浏览器
    import javax.servlet.*;
    import javax.servlet.http.*;
    import javax.servlet.annotation.*;
    import java.io.FileInputStream;
    import java.io.IOException;@WebServlet("/RequestDemo1")
    public class RequestDemo1 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {FileInputStream fis = new FileInputStream("D:\\background.jpg");ServletOutputStream os = response.getOutputStream();byte[] buffer = new byte[1024];int len = 0;while ((len = fis.read(buffer)) != -1){os.write(buffer,0,len);}fis.close();//不需要关闭ServletOutputStream,response会帮我们关闭}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request, response);}
    }

 

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

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

相关文章

Springboot 实现热部署

spring为开发者提供了一个名为spring-boot-devtools的模块来使Spring Boot应用支持热部署&#xff0c;提高开发者的开发效率&#xff0c;无需手动重启Spring Boot应用。引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>s…

虚拟机扩展磁盘容量后扩展分区大小

1. 首先检查磁盘剩余空间sudo fdisk -l /dev/sda2. 如果有未分配空间&#xff0c;直接扩展分区sudo fdisk /dev/sda在fdisk交互界面中&#xff0c;依次写入d # 删除分区 3 # 例如选择分区3&#xff08;/dev/sda3&#xff09; n # 新建分区 p # 主分区 3 # 分区号3 # 起始扇…

元宇宙与游戏:虚实交融的数字文明新纪元

引言&#xff1a;当游戏遇见元宇宙在纽约现代艺术博物馆&#xff08;MoMA&#xff09;的"虚拟世界"特展中&#xff0c;一幅数字艺术作品《元宇宙诞生》引发观展热潮。这幅由AI与人类艺术家共同创作的作品&#xff0c;描绘了游戏《堡垒之夜》的虚拟演唱会与现实世界交…

音视频学习(四十二):H264帧间压缩技术

必要性与优势 原始数字视频数据量庞大&#xff0c;未经压缩的视频难以有效传输和存储。例如&#xff0c;一个 1080p、30fps 的无压缩视频&#xff0c;每秒数据量可达数百兆比特。视频压缩的目标是在保证视觉质量的前提下&#xff0c;最大限度地减少数据冗余。视频数据中存在多种…

微服务雪崩防护最佳实践之sentinel

思考1、当服务访问量达到一定程度&#xff0c;流量扛不住的时候&#xff0c;该如何处理&#xff1f;2、服务之间相互依赖&#xff0c;当服务A出现响应时间过长&#xff0c;影响到服务B的响应&#xff0c;进而产生连锁反应&#xff0c;直至影响整个依赖链上的所有服务&#xff0…

阿里云监控及运维常见问题

云监控介绍&#xff1a;阿里云的云监控服务&#xff08;CloudMonitor&#xff09;是一款简单易用、功能强大的监控工具&#xff0c;主要用来帮助用户实时监控阿里 云上的各种资源&#xff08;比如服务器、数据库、网络等&#xff09;&#xff0c;并在出现问题时及时发出警报&am…

深入解析 vLLM 分布式推理与部署策略

在当今人工智能快速发展的时代&#xff0c;大型语言模型&#xff08;LLM&#xff09;的推理和部署面临着诸多挑战&#xff0c;尤其是当模型规模日益庞大时&#xff0c;如何高效地利用硬件资源成为关键问题。vLLM 作为一种强大的工具&#xff0c;为分布式推理和部署提供了多种策…

PostgreSQL 终端命令详解及实际应用案例

PostgreSQL 作为一款功能强大的开源关系型数据库&#xff0c;其终端命令是数据库管理员、开发人员日常操作的核心工具。这些命令覆盖数据库连接、对象管理、数据操作、备份恢复、权限控制等多个维度&#xff0c;掌握其实际应用场景能显著提升工作效率。本文结合不同行业的实操案…

步进电机基础

1、ENABLE‾\overline{ENABLE}ENABLE 的意思&#xff1a; ENABLE上面的横线表示这是一个低电平有效的信号&#xff1a;当ENABLE信号为低电平&#xff08;0V或逻辑0&#xff09;时&#xff0c;芯片被使能&#xff08;激活&#xff09;当ENABLE信号为高电平&#xff08;VDD或逻辑…

Redis进阶--缓存

目录 一、引言 二、介绍 1.为什么Mysql的速度慢呢&#xff1f; 2.缓存更新策略 1.定期生成 2.实时生成 3.redis的内存淘汰机制 4.注意事项 1.缓存预热 2.缓存穿透 3.缓存击穿 4.缓存雪崩 三、总结 一、引言 本篇文章将介绍Redis其中一个重要的作用&#xff0c;作为…

微软原版系统下载的几个好用网站

“不要因为走得太远&#xff0c;而忘记为什么出发。” 愿每一位Windows用户&#xff0c;都能在这份情怀中找到属于自己的小确幸&#xff01;满满的情怀&#xff01; 微软官方渠道 微软官网提供纯净的ISO镜像下载&#xff0c;适用于Windows 10/11等系统。访问Microsoft官网下…

kotlin Flow快速学习2025

其实&#xff0c;第一章节&#xff0c;只是让你了解下Flow的基本情况。我们开发中&#xff0c;基本很少使用这种模式。所以来讲&#xff0c;我们甚至可以直接使用StateFlow和SharedFlow才是正途。这是很多教程没有说明的点。所以第一章随便浏览下即可。日后再补充理解都是可以的…

【人工智能99问】什么是教师强制?(16/99)

文章目录什么是教师强制&#xff1f;教师强制&#xff08;Teacher Forcing&#xff09;的定义原比例&#xff08;Original Proportion&#xff09;教师强制的举例说明&#xff08;一&#xff09;教师强制的举例说明&#xff08;二&#xff09;优点和缺点解决曝光偏差的方法什么…

【WPF】WPF 自定义控件之依赖属性

&#x1f4e6; WPF 自定义控件之依赖属性 在开发 WPF 应用时&#xff0c;自定义控件能帮助我们复用逻辑和样式&#xff0c;但我很快会遇到一个问题&#xff1a;在控件内部如何支持数据绑定和属性变更通知&#xff1f;特别是我们继承自 Control 的时候&#xff0c;已经不能再继承…

DOM型XSS破坏

目录 首先 然后 第一种 第二种&#xff08;DOM&#xff09; HTMLCollection HTML Relationships Custom 解 首先 <script>//urlencode解码 //location接口的hash属性是一个字符串&#xff0c;包含一个“#”后跟位置URL的片段标识符。如果URL没有片段标识符&#…

Linux C 多线程基本操作

我们已经了解进程的基本概念&#xff1a;进程是正在执行的程序&#xff0c;并且是系统资源分配的基本单位。当用户需要在一台计算机上去完成多个独立的工作任务时&#xff0c;可以使用多进程的方式&#xff0c;为每个独立的工作任务分配一个进程。多进程的管理则由操作系统负责…

C语言基础:二维数组练习题

1. 一个二维数组赋了初值&#xff0c;用户输入一个数&#xff0c;在该二维数组中查找。找到则返回行列位置&#xff0c;没找到则提示。#include <stdio.h>int main() {int arr[3][3] {{1, 2, 3},{4, 5, 6},{7, 8, 9}};int t;printf("要查找的数&#xff1a;")…

Java面试题034:一文深入了解MySQL(6)

Java面试题029&#xff1a;一文深入了解MySQL&#xff08;1&#xff09; Java面试题030&#xff1a;一文深入了解MySQL&#xff08;2&#xff09; Java面试题031&#xff1a;一文深入了解MySQL&#xff08;3&#xff09; Java面试题032&#xff1a;一文深入了解MySQL&#x…

Java基础教程(011):面向对象中的构造方法

10-面向对象-构造方法 构造方法也叫做构造器、构造函数。 作用&#xff1a;在创建对象的时候给成员变量进行初始化的。 ✅ 一、构造方法的特点特点说明与类同名构造方法的名称必须与类名相同没有返回类型构造方法没有返回值&#xff0c;甚至不能写 void自动调用使用 new 创建对…

Adobe Photoshop:数字图像处理的终极工具指南

Hi&#xff0c;我是布兰妮甜 &#xff01;Adobe Photoshop自1990年问世以来&#xff0c;已经成为数字图像处理领域的标杆和代名词。这款强大的软件不仅彻底改变了摄影、设计和艺术创作的方式&#xff0c;还深刻影响了我们消费和感知视觉内容的文化方式。从专业摄影师到社交媒体…