servlet与J2EE
J2EE中提出了容器和组件的概念,在JavaEE平台上,处理TCP连接,解析HTTP协议这些底层工作统统扔给现成的Web容器去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet
API,我们使用Servlet
API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层网络连接功能。
与jdbc类似,我们编写符合jdbc接口的程序,由具体的厂商实现jdbc驱动。
要注意我们编写的Servlet并不是直接运行的,而是由Web服务器加载后创建servlet实例运行,所以,类似Tomcat这样的Web服务器(容器)也称为Servlet容器。
最简单的servlet
我们来实现一个最简单的Servlet:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}
我们编写的Servlet
总是继承自HttpServlet
,然后覆写doGet()或doPost()方法。注意到doGet()方法传入了HttpServletRequest
和HttpServletResponse
两个对象,分别代表HTTP请求和响应。 我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter,写入响应即可。
由于Web服务器会使用多线程的方式执行Servlet请求,因此要正确编写Servlet,需要清晰理解Java的多线程模型:
- 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
- HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
- 在doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。
启动嵌入式Tomcat并加载当前工程的webapp
参考:Servlet开发 ❓对于本节并不是很理解;
Servlet进阶
每个Servlet
通过注解说明自己能处理的路径。例如:
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
...
}
HelloServlet
能处理/hello
这个路径的请求。 要处理GET、POST、PUT、DELETE等不同类型的请求,需要覆写doGet()
、doPost()
、doPut()
等方法;当没有覆写时,会直接返回405或400错误。
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
...
}
}
HttpServletRequest
有了HttpServletRequest和HttpServletResponse这两个高级接口,我们就不需要直接处理HTTP协议。注意到具体的实现类是由各服务器提供的,而我们编写的Web应用程序只关心接口方法,并不需要关心具体实现的子类。
HttpServletRequest
封装了一个HTTP请求,通过HttpServletRequest
提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:
getMethod()
:返回请求方法,例如,"GET","POST";getRequestURI()
:返回请求路径,但不包括请求参数,例如,"/hello";getQueryString()
:返回请求参数,例如,"name=Bob&a=1&b=2";getParameter(name)
:返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;getContentType()
:获取请求Body的类型,例如,"application/x-www-form-urlencoded";getContextPath()
:获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串"";getCookies()
:返回请求携带的所有Cookie;getHeader(name)
:获取指定的Header,对Header名称不区分大小写;getHeaderNames()
:返回所有Header名称;getInputStream()
:如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;getReader()
:和getInputStream()类似,但打开的是Reader;getRemoteAddr()
:返回客户端的IP地址;getScheme()
:返回协议类型,例如,"http","https";
HttpServletResponse
HttpServletResponse
封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。 写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。 写入完毕后调用flush()是必须的,因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。此外,写入完毕后千万不要调用close(),原因同样是因为会复用TCP连接,如果关闭写入流,将关闭TCP连接,使得Web服务器无法复用此TCP连接。
Servlet多线程模型
一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。 因此,如果Servlet中定义了字段,要注意多线程并发访问的问题:
重定向
在HTTP协议中,重定向是一种常见的机制,服务器可以通过返回特定的状态码(如301
或302
)来告诉客户端需要跳转到另一个URL。
301 Moved Permanently
:表示请求的资源已经永久移动到了新的地址。302 Found
:表示请求的资源暂时性地移动到了一个新的地址。
浏览器遇到重定向时的处理过程通常如下:
- 接收到重定向响应后,浏览器根据重定向状态码(301或302)决定是否跳转。
- 对于301,浏览器会直接跳转到新的URL,并且将新URL加入浏览器历史记录中。
- 对于302,浏览器会发送新的请求到重定向的地址,获取重定向后的内容,并更新地址栏显示新URL。
要注意
axios
或fetch
API是无法在.then
中直接处理重定向前的请求的,浏览器会根据新的地址直接重新发送请求,所以在.then
中只能处理重定向后的请求。
在servlet
中,我们可以很容易的编写重定向的功能:
@WebServlet(urlPatterns = "/hi")
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 构造重定向的路径:
String redirectToUrl = "/hello";
// 发送重定向响应:
resp.sendRedirect(redirectToUrl);
}
}
转发
当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/hello").forward(req, resp);
}
}
使用servlet实现一个简易mvc框架
Servlet提供的接口仍然偏底层,直接使用servelt编写的java web应用会导致业务代码与网络请求的代码耦合在一起,不利于开发。所以能不能直接使用普通的java类来编写业务逻辑? MVC
(Model-View-Controller)模式是一种软件设计模式,常用于web应用程序的开发,将应用程序分解为三个主要部分:模型(Model)
、视图(View)
和控制器(Controller)
,从而实现了数据、业务逻辑与视图层的松耦合。
- 模型(Model):代表应用程序的数据结构、业务逻辑和状态。
- 视图(View):负责展示数据
- **控制器:**充当模型和视图之间的中介,将用户的请求转发给模型进行处理,并将处理结果返回给视图显示给用户。
在Java中,Spring MVC就是基于MVC模式构建的,而Spring MVC的底层是基于Servlet实现的,我们来看看它的核心开发思想和原理。
mvc框架的基本特性
- 通过普通的java类实现Controller,同时对于不同的请求方法(get、post),通过
**@GetMapping**
、** @PostMapping**
等注解标注的方法进行处理。
public class UserController {
@GetMapping("/signin")
public ModelAndView signin() {
...
}
@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
}
- 对于HttpServletRequest、HttpServletResponse、HttpSession,只要方法参数有定义,就可以自动传入:
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
动手 设计mvc框架
- 构造一个
DispatcherServlet
作为一个控制中心,它总是映射到/
,作为一个入口负责将所有请求的转发给对应controller - 扫描包中的controller注解,获取
@GetMapping
等注解指代的路径,构造Map<String url,dispatch>
,用于路径的映射
实现DispatcherServlet
DispatchServlet
是接受所有的请求的servelet,并根据controller中定义的path决定调用那个方法。作用架构如下图: 首先,我们需要存储请求路径到某个具体方法的映射:
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
}
处理一个GET请求是通过GetDispatcher对象完成的,它需要如下信息:
class GetDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
String[] parameterNames; // 方法参数名称
Class<?>[] parameterClasses; // 方法参数类型
}
GetDispatcher
还需要定义invoker方法处理真正的请求:
class GetDispatcher {
...
// 基本思想是通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
String parameterName = parameterNames[i];
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else if (parameterClass == int.class) {
arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == long.class) {
arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == boolean.class) {
arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
} else if (parameterClass == String.class) {
arguments[i] = getOrDefault(request, parameterName, "");
} else {
throw new RuntimeException("Missing handler for type: " + parameterClass);
}
}
return (ModelAndView) this.method.invoke(this.instance, arguments);
}
private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
String s = request.getParameter(name);
return s == null ? defaultValue : s;
}
}
postDispatch
的实现基本相同。 接下来,我们来实现整个DispatcherServlet的处理流程,以doGet()为例:
public class DispatcherServlet extends HttpServlet {
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
String path = req.getRequestURI().substring(req.getContextPath().length());
// 根据路径查找GetDispatcher:
GetDispatcher dispatcher = this.getMappings.get(path);
if (dispatcher == null) {
// 未找到返回404:
resp.sendError(404);
return;
}
// 调用Controller方法获得返回值:
ModelAndView mv = dispatcher.invoke(req, resp);
// 允许返回null:
if (mv == null) {
return;
}
// 允许返回`redirect:`开头的view表示重定向:
if (mv.view.startsWith("redirect:")) {
resp.sendRedirect(mv.view.substring(9));
return;
}
// 将模板引擎渲染的内容写入响应:
PrintWriter pw = resp.getWriter();
this.viewEngine.render(mv, pw);
pw.flush();
}
}
最后一步是在DispatcherServlet
的init()
方法中初始化所有Get
和Post
的映射,以及用于渲染的模板引擎:
public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
private ViewEngine viewEngine;
@Override
public void init() throws ServletException {
this.getMappings = scanGetInControllers();
this.postMappings = scanPostInControllers();
this.viewEngine = new ViewEngine(getServletContext());
// 使用反射扫描所有Controller以获取所有标记有@GetMapping和@PostMapping的方法
// 构造getMappings和postMappings
// ...
}
...
}
filter
JavaEE的Servlet
规范还提供了一种Filter
组件,即过滤器,它的作用是,在HTTP请求到达Servlet之前,可以被一个或多个Filter预处理。类似打印日志、登录检查等逻辑,完全可以放到Filter中。 我们编写一个最简单的EncodingFilter,它强制把输入和输出的编码设置为UTF-8:
@WebFilter(urlPatterns = "/*")
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("EncodingFilter:doFilter");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
编写Filter时,必须实现Filter
接口,在doFilter()
方法内部,要继续处理请求,必须调用chain.doFilter()
。最后,用@WebFilter
注解标注该Filter
需要过滤的URL。这里的/*
表示所有路径。 添加了Filter
之后,整个请求的处理架构如下: 除了使用/*
处理所有路径的请求,也可以编写特定路径进行过滤的Filter
,比如说下边这段代码可以只过滤以/user/开头的路径。
@WebFilter("/user/*")
public class AuthFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//......
}
}
listen
除了Servlet
和Filter
外,JavaEE的Servlet规范还提供了第三种组件:Listener
。 有几种Listener:
ServletContextListener
:监听ServletContext
的创建和销毁事件;HttpSessionListener
:监听HttpSession
的创建和销毁事件;ServletRequestListener
:监听ServletRequest
请求的创建和销毁事件;ServletRequestAttributeListener
:监听ServletRequest
请求的属性变化事件(即调用ServletRequest.setAttribute()方法);ServletContextAttributeListener
:监听ServletContext
的属性变化事件(即调用ServletContext.setAttribute()方法);
任何标注为@WebListener
,且实现了特定接口的类会被Web服务器自动初始化。我们编写一个实现了ServletContextListener
接口的类如下:
@WebListener
public class AppListener implements ServletContextListener {
// 在此初始化WebApp,例如打开数据库连接池等:
public void contextInitialized(ServletContextEvent sce) {
System.out.println("WebApp initialized.");
}
// 在此清理WebApp,例如关闭数据库连接池等:
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("WebApp destroyed.");
}
}