Servlet 处理 HTTP 请求的流程
一般情况下,浏览器(客户端)通过 HTTP 协议来访问服务器的资源,Servlet 主要用来处理 HTTP 请求。核心对象有三个
- Servlet:提供service()方法处理请求
- ServletRequest:请求信息载体
- ServletRespond:响应信息载体
- Servlet 容器接收到来自客户端的 HTTP 请求后,Servlet容器会针对该请求分别创建一个 HttpServletRequest 对象和 HttpServletReponse 对象。
- 容器将 HttpServletRequest 对象和 HttpServletReponse 对象以参数的形式传入 service() 方法内,并调用该方法。
- 在 service() 方法中 Servlet 通过 HttpServletRequest 对象获取客户端信息以及请求的相关信息。
- 对 HTTP 请求进行处理。
- 请求处理完成后,将响应信息封装到 HttpServletReponse 对象中。
- Servlet 容器将响应信息返回给客户端。
- 当 Servlet 容器将响应信息返回给客户端后,HttpServletRequest 对象和 HttpServletReponse 对象被销毁。
HttpServletRequest 和 HttpServletReponse 是 Servlet 处理 HTTP 请求流程中最重要的两个对象。HttpServletRequest 对象用于封装 HTTP 请求信息,HttpServletReponse 对象用于封装 HTTP 响应信息。
ServletRequest
ServletRequest接口是Java Servlet API中的一个接口,用于访问Servlet容器接收到的HTTP请求的信息。ServletRequest接口定义了一系列方法,用于获取HTTP请求的参数、属性、输入流等信息,以及对这些信息的操作。所以想要从请求中获取信息,得从ServletRequest 中去寻找。
- getProtocol():返回请求使用的协议的名称和版本号。例如,HTTP/1.1。
- getScheme():返回请求的协议名称。例如,http、https。
- getServerName():返回接收请求的服务器的名称。
- getServerPort():返回接收请求的服务器的端口号。
- getRemoteAddr():返回客户端的IP地址。
- getRemoteHost():返回客户端的主机名。
- getRemotePort():返回客户端的端口号。
- getLocalAddr():返回服务器的IP地址。
- getLocalName():返回服务器的主机名。
- getLocalPort():返回服务器的端口号。
- getParameter(String name):返回请求参数的值,如果请求参数不存在,则返回null。
- getParameterNames():返回请求参数的名称的枚举。
- getParameterValues(String name):返回请求参数的值的数组,如果请求参数不存在,则返回null。
- getAttribute(String name):返回指定属性名称的属性值,如果属性不存在,则返回null。
- getAttributeNames():返回所有属性名称的枚举。
- setAttribute(String name, Object value):将指定属性名称的属性值设置为指定的值。
- removeAttribute(String name):从请求中删除指定名称的属性。
- getLocale():返回客户端的首选语言环境。
- getLocales():返回客户端的所有语言环境。
- getCharacterEncoding():返回请求字符编码的名称,如果字符编码未指定,则返回null。
- getContentLength():返回请求的正文的长度,以字节为单位。
- getContentType():返回请求正文的MIME类型。
- getInputStream():返回请求正文的InputStream。
- getReader():返回请求正文的Reader。
- getProtocol():返回请求使用的协议的名称和版本号。
- getRemoteUser():返回发出请求的用户的登录名,如果用户未经过身份验证,则返回null。
- isSecure():返回请求是否通过安全连接传输。
- getRequestDispatcher(String path):返回用于将请求转发到另一个资源的RequestDispatcher对象。
- getRealPath(String path):返回指定虚拟路径的真实路径。
- getRequestURI():返回请求的URI,不包括查询字符串。
- getRequestURL():返回请求的URL,包括协议,服务器名称,端口号和请求URI,但不包括查询字符串。
- getServletPath():返回Servlet的路径。
- getSession():返回与此请求相关联的会话,如果请求没有会话,则创建一个新会话。
- getSession(boolean create):返回与此请求相关联的会话,如果请求没有会话,则根据指定的create参数创建一个新会话或不创建。
- isRequestedSessionIdValid():返回请求的会话ID是否仍然有效。
- isRequestedSessionIdFromCookie():返回请求是否使用Cookie来检索会话ID。
- isRequestedSessionIdFromURL():返回请求是否将会话ID附加到URL中。
- isRequestedSessionIdFromUrl():已过时的方法,等效于isRequestedSessionIdFromURL()。
HttpServletRequest
在 Servlet API 中,定义了一个 HttpServletRequest 接口,它继承自 ServletRequest 接口。HttpServletRequest 专门用于封装 HTTP 请求消息,该接口在ServletRequest接口的基础上增加了许多HTTP特定的方法,以提供更多的HTTP请求和响应相关的信息。部分新增信息如下:
- getMethod():返回HTTP请求的方法类型(GET、POST等)。
- getPathInfo():返回HTTP请求的路径信息。
- getPathTranslated():返回HTTP请求的路径翻译后的信息。
- getQueryString():返回HTTP请求的查询字符串。
- getContentLength():返回HTTP请求的主体内容长度。
- getContentType():返回HTTP请求的主体内容类型。
- getServletPath():返回HTTP请求的Servlet路径。
- getRequestURI():返回HTTP请求的统一资源标识符(URI)。
- getRequestURL():返回HTTP请求的统一资源定位器(URL)。
- getProtocol():返回HTTP请求的协议名称和版本号。
- getRemoteAddr():返回HTTP请求的客户端IP地址。
- getRemoteHost():返回HTTP请求的客户端主机名。
- getServerName():返回HTTP请求的服务器名称。
- getServerPort():返回HTTP请求的服务器端口号。
- getScheme():返回HTTP请求使用的协议名称(http、https等)。
- isSecure():返回HTTP请求是否通过安全套接字层(SSL)进行传输。
URI和URL的区别
- URI(Uniform Resource Identifier,统一资源标识符)是一个标识某个资源的字符串,可以用来唯一地标识一个资源,包括但不限于Web页面、图片、文本文件等。URI包括两个部分:URL和URN。URN(Uniform Resource Name,统一资源名称)是通过名字来标识资源的。
- URL(Uniform Resource Locator,统一资源定位符)则是通过地址来标识资源的,即URL是一种特定类型的URI,它通过指定地址来标识特定的资源。例如,"https://www.example.com/index.html"是一个URL,它指定了要访问的资源的地址和协议。
因此,**URL是URI的一个子集,所有的URL都是URI,但不是所有的URI都是URL。URI还可以表示一些没有直接关联到网络资源的抽象概念,例如一个人的名字或者一份文档的版本号。**而URL则是用来指定如何访问Web资源的字符串,包括协议、主机名、端口号、路径和查询参数等。
Tomcat 中创建 HttpServletRequest
在 Tomcat 中,HttpServletRequest 对象的创建是由 Catalina 处理请求的核心组件 org.apache.catalina.connector.Connector 负责的。具体来说,当 Connector 接收到客户端请求时,会创建 Request 对象(该对象封装了 HttpServletRequest 对象)并将其传递给 Tomcat 的管道处理器 Pipeline 进行处理。Pipeline 中会将 Request 对象传递给多个阶段的处理器(Valve),在每个阶段中对 Request 对象进行处理并执行相应的操作(如安全认证、过滤器等),最终将 Request 对象传递给对应的 Servlet 进行业务逻辑处理。
具体创建 HttpServletRequest 对象的源码如下所示:
@Override
public void service(SocketWrapperBase<?> socket) throws IOException {// ...// 创建 Request 对象并将其封装到 Processor 实例中Http11Processor processor = createProcessor();processor.assign(socket);if (proto.isSSLEnabled() && (socket instanceof SecureNio2Channel)) {SecureNio2Channel channel = (SecureNio2Channel) socket;SSLEngine engine = proto.getSSLEngine();if (engine != null) {channel.reset(engine);}}// 将请求交给 Pipeline 处理器进行处理pipeline.getFirst().invoke(processor.getRequest(), processor.getResponse());// ...
}
从上述代码中可以看出,当接收到客户端请求时,Tomcat 会通过 createProcessor 方法创建 Http11Processor 实例,并将 SocketWrapperBase 作为参数传递给该实例。在 Http11Processor 中,会调用 createRequest 方法创建 Request 对象,并将 SocketWrapperBase 和 Http11InputBuffer 封装到 Request 对象中。最终,Tomcat 将 Request 对象作为参数传递给 Pipeline 处理器进行处理。在 Pipeline 中,会将 Request 对象传递给多个 Valve 进行处理,最终将 Request 对象传递给对应的 Servlet 进行业务逻辑处理。
Servlet接收请求中创建的对象
- HttpServletRequest 对象:封装了客户端请求的信息,包括请求参数、请求头、请求体等信息。
- HttpServletResponse 对象:封装了响应信息,包括响应状态码、响应头、响应体等信息。
- ServletContext 对象:代表 Servlet 上下文,可以用于在不同 Servlet 之间共享数据。
- HttpSession 对象:代表客户端的会话,可以用于在同一客户端多次请求之间共享数据。
- ServletConfig 对象:代表 Servlet 的配置信息,包括 Servlet 的初始化参数等信息。
- ServletRequest 对象:是 HttpServletRequest 的父接口,定义了通用的请求方法,如获取请求参数等。
- ServletResponse 对象:是 HttpServletResponse 的父接口,定义了通用的响应方法,如设置响应头等。
以上对象都是由 Servlet 容器在接收到客户端请求时创建的,可以在 Servlet 中直接使用。其中,HttpServletRequest 和 HttpServletResponse 对象是每个请求独有的,而 ServletContext 和 HttpSession 对象则是在多个请求之间共享的。
这就是为什么springboot中可以直接获取这两个对象,这两个对象不需要传入。
需要注意的是,具体创建对象的方式可能因 Servlet 容器而异。例如,在 Tomcat 中,HttpServletRequest 和 HttpServletResponse 对象是由 Catalina 处理请求的核心组件 org.apache.catalina.connector.Connector 负责创建的,而 ServletContext 和 HttpSession 对象则是由 org.apache.catalina.core.StandardContext 创建的。
接口中怎么使用
@RequestMapping("name2")
public Object task2() {log.info("start");RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();System.out.println(request.getHeader("Host"));return name2;
}
@RequestMapping("name2")
public Object task2( HttpServletRequest request, HttpServletResponse response) {log.info("start");System.out.println(request.getHeader("Host"));return name2;
}
@Autowired
HttpServletRequest request;@RequestMapping("name2")
public Object task2() {log.info("start");System.out.println(request.getHeader("Host"));return name2;
}
以上伪代码的作用是一致的,无论在接口上写参数还是方法内创建还是自动注入,都不会影响调用者,因为这两个对象是Servlet创建和销毁的,可以直接使用。
spring中直接注入HttpServletRequest
spring中居然可以将HttpServletRequest直接通过@Autowired注入,突然有点懵。
-
HttpServletRequest并非Spring中的类,且在没有手动通过@Bean的方式注入,Spring是怎么做到帮开发者完成注入的?理论上,我们需要注入其他包中的对象的时候,是必须在@Configuration修饰的类下面使用@Bean进行方法修饰的才行的
@Configuration public class ApplicationConfig {@Beanpublic RestTemplate getRestTemplate(){return new RestTemplate();} }
-
同时,ioc容器中默认注入的Bean是单例,而每个请求都是独立的,这样不会出问题吗?
demo分析
@RestController
public class HttpServletRequestTest {@Autowiredprivate HttpServletRequest httpServletRequest;@GetMapping("/sayHi")public void sayHi(String name) {System.out.println(httpServletRequest.getRequestURL().toString());JSONObject res = new JSONObject();res.put("requestId", UUID.randomUUID().toString());res.put("datatime", DateUtils.getDatetime());// ReturnResult.get(res);System.out.println("hello: " + name);}@PostConstructpublic void after(){System.out.println(this.httpServletRequest);}}
- @PostConstruct:后构造注解,见名知意,标注于方法之上,在对象加载完依赖注入后执行,常用于初始化连接源等信息。是java的注解。在Servlet生命周期中有一定作用,它通常都是一些初始化的操作,但初始化可能依赖于注入的其他组件,所以要等依赖全部加载完再执行。
- @PreDestroy:前销毁注解,同理后构造
- 在spring项目中,在一个bean的初始化过程中,方法执行先后顺序为Constructor > @Autowired > @PostConstruct > @PreDestroy
Spring不是只能自动注入被管理的对象吗?
作为一个外部对象,HttpServletRequest为什么可以在Spring项目中通过注入的方式获取?
@PostConstruct注解被用在执行完依赖注入之后的方法调用上,我们将断点打在上述demo的第19行,即可查看HttpServletRequest httpServletRequest实例化之后的情况。
由上图我们可以看到这个httpServletRequest对象是一个代理对象(org.springframework.web.context.support.WebApplicationContextUtils.RequestObjectFactory),该对象是一个请求的对象工厂。进入WebApplicationContextUtils这个类,Command + F12我们发现了一个往 ConfigurableListableBeanFactory工厂对象注入bean对象的方法registerWebApplicationScopes。
该方法有这样一行代码:
beanFactory.``registerResolvableDependency(ServletRequest.class, new RequestObjectFactory()``);
由于HttpServletRequest正是继承自ServletRequest,这里引起了我们的关注。
ConfigurableListableBeanFactory的registerResolvableDependency方法又是用什么的呢?我们继续往下看。
使用相应的自动装配值注册一个特殊的依赖类型。
这适用于被认为可自动装配但未在工厂中定义为 bean 的工厂/上下文引用
原来Spring可以为一个类注入另一个完全不同的对象值,这样从ioc容器中引用这个类的时候其实拿到的就是这个对象值。且上面的描述正好回应了,为什么我们没有手动定义HttpServletRequest却可以完成它的自动装配,秘诀就在这里。
进一步拓展思考,上面两张图一起看,除了ServletRequest.class,ServletResponse.class,HttpSession.class以及WebRequest.class类型对象,均被Spring自动注入,可以直接通过注解的方式引用。
继续进入registerResolvableDependency方法,我们发现该方法的实现是将ServletRequest.class的依赖类型作为key, RequestObjectFactory作为装配的value,放入了resolvableDependencies这个map中。
Spring中的对象不是单例吗?
HttpServletRequest注入和引用我们知道了,那么针对每个独立的请求,多线程场景下,通过自动注入的方式,HttpServletRequest 是否会有线程安全的问题呢?
我们已经知道注入拿到的HttpServletRequest返回的对象其实是下面这个
org.springframework.web.context.support.WebApplicationContextUtils.``RequestObjectFactory对象,那么它是怎么做到线程安全的呢?
我们看它的具体实现。
沿着currentRequestAttributes()方法一路点击,最终发现它的返回值,是来自org.springframework.web.context.request.RequestContextHolder#requestAttributesHolder。
requestAttributesHolder对象是一个ThreadLocal对象,由此我们明白了,Spring自动注入的HttpServletRequest能够保证请求的唯一性,原来是通过跟每个线程绑定了,从ThreadLocal中取得请求信息,由此保证的线程安全!
我们再来拓展一下,既然请求信息是从ThreadLocal中取的,那么请求信息是如何放进去的呢?
我们找到requestAttributesHolder属性的set方法,在这里打一个断点,重启系统,并发起一个http请求。
最终,通过调用堆栈信息,我们看到了org.springframework.web.filter.RequestContextFilter#doFilterInternal
中调用了initContextHolder方法,而initContextHolder方法调用了requestAttributesHolder这个ThreadLocal的set方法。
每个http请求过来会进入RequestContextFilter这个filter,在这个filter中会将request信息设置到当前的线程里。
总结
-
在代码中通过注解注入HttpServletRequest的方式,拿到的其实并不是真正的 HttpServletRequest,而是一个Spring项目启动时自动注入的代理对象org.springframework.web.context.support.``WebApplicationContextUtils.RequestObjectFactory。
该对象跟ServletRequest.class做了映射关联,放入了Spring管理bean注入的map中。private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {@Overridepublic ServletRequest getObject() {// 获取当前 ServletRequestreturn currentRequestAttributes().getRequest();}@Overridepublic String toString() {return "Current HttpServletRequest";}}private static ServletRequestAttributes currentRequestAttributes() {RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();if (!(requestAttr instanceof ServletRequestAttributes)) {throw new IllegalStateException("Current request is not a servlet request");}return (ServletRequestAttributes) requestAttr;} }// 那这个“当前 ServletRequest”从哪来呢? //Servlet 除了 ServletContextListener 之外,还有一个 ServletRequestListener:public interface ServletRequestListener extends EventListener {void requestDestroyed(ServletRequestEvent var1);void requestInitialized(ServletRequestEvent var1); } //只要你注册了这个,就能在请求前执行requestDestroyed //Spring 弄了一个 org.springframework.web.context.request.RequestContextListener,在这个里面获取并存储的@Overridepublic void requestInitialized(ServletRequestEvent requestEvent) {if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {throw new IllegalArgumentException("Request is not an HttpServletRequest: " + requestEvent.getServletRequest());}HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();ServletRequestAttributes attributes = new ServletRequestAttributes(request);request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);LocaleContextHolder.setLocale(request.getLocale());// 存储 ServletRequestRequestContextHolder.setRequestAttributes(attributes);}
-
每次请求时,如何保证自动注入的HttpServelet请求线程安全,等价于问 org.springframework.web.context.support.WebApplicationContextUtils.``RequestObjectFactory 怎么做到的线程安全。这个类只是一个简单的对象工厂,getObject方法最终从org.springframework.web.context.request.RequestContextHolder#requestAttributesHolder这个ThreadLocal中获取请求的信息,由此做到了请求之间的隔离。
-
其实spring中还有很多类可以直接注入:import org.springframework.cloud.client.discovery.DiscoveryClient。
-
直接注入和需要@Bean修饰之后才能注入的区别:一个是内部实现好了,默认已经放进去了;一个是我们业务需求自己放进去的,也就是你要把这个bean交给spring管理,这就是spring ioc 控制反转。
-
特殊情况特殊处理。不要把ioc死板的搬过来。Request, Response, HttpEntity等都是特殊处理的。而且这几个对象和请求有关,不在窗口里面。