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,写入响应即可。
由于正确编写Servlet,需要清晰理解Java的多线程模型:
- 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
- HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
- 在doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。
参考:Servlet开发
❓对于本节并不是很理解;
Servlet进阶
``@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
...
}
HelloServlet
能处理/hello
这个路径的请求。
要处理GET、POST、PUT、DELETE等不同类型的请求,需要覆写doGet()
、doPost()
、doPut()
等方法;当没有覆写时,
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
...
}
}
<font style="color:rgb(51, 51, 51);background-color:rgb(250, 250, 250);">HttpServletRequest</font>
HttpServletRequest
<font style="color:rgb(51, 51, 51);">getMethod()</font>
<font style="color:rgb(51, 51, 51);">getRequestURI()</font>
<font style="color:rgb(51, 51, 51);">getQueryString()</font>
<font style="color:rgb(51, 51, 51);">getParameter(name)</font>
<font style="color:rgb(51, 51, 51);">getContentType()</font>
<font style="color:rgb(51, 51, 51);">getContextPath()</font>
<font style="color:rgb(51, 51, 51);">getCookies()</font>
<font style="color:rgb(51, 51, 51);">getHeader(name)</font>
<font style="color:rgb(51, 51, 51);">getHeaderNames()</font>
<font style="color:rgb(51, 51, 51);">getInputStream()</font>
<font style="color:rgb(51, 51, 51);">getReader()</font>
<font style="color:rgb(51, 51, 51);">getRemoteAddr()</font>
<font style="color:rgb(51, 51, 51);">getScheme()</font>
HttpServletResponse
封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。
写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。
写入完毕后调用flush()是必须的,因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。此外,写入完毕后千万不要调用close(),原因同样是因为会复用TCP连接,如果关闭写入流,将关闭TCP连接,使得Web服务器无法复用此TCP连接。
********````<font style="color:rgb(32, 35, 39);">301 Moved Permanently</font>
<font style="color:rgb(32, 35, 39);">302 Found</font>
要注意
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);
}
}
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/hello").forward(req, resp);
}
}
直接使用servelt编写的java web应用会导致业务代码与网络请求的代码耦合在一起,不利于开发。所以能不能直接使用普通的java类来编写业务逻辑?
<font style="color:rgb(32, 35, 39);">MVC</font>
<font style="color:rgb(32, 35, 39);">模型(Model)</font>
<font style="color:rgb(32, 35, 39);">视图(View)</font>
<font style="color:rgb(32, 35, 39);">控制器(Controller)</font>
- 通过普通的java类实现**
**<font style="color:rgb(51, 51, 51);">@GetMapping</font>**
**<font style="color:rgb(51, 51, 51);"> @PostMapping</font>**
**
public class UserController {
@GetMapping("/signin")
public ModelAndView signin() {
...
}
@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
}
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
动手 设计mvc框架
- 构造一个
DispatcherServlet
作为一个控制中心,它总是映射到/
,作为一个入口负责将所有请求的转发给对应controller - 扫描包中的controller注解,获取
@GetMapping
等注解指代的路径,构造Map<String url,dispatch>
,用于路径的映射
实现
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<>();
}
class GetDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
String[] parameterNames; // 方法参数名称
Class<?>[] parameterClasses; // 方法参数类型
}
<font style="color:rgb(32, 35, 39);">GetDispatcher</font>
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;
}
}
<font style="color:rgb(32, 35, 39);">postDispatch</font>
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();
}
}
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
// ...
}
...
}
````********@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
之后,整个请求的处理架构如下:
@WebFilter("/user/*")
public class AuthFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//......
}
}
除了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.");
}
}