Skip to content

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:

java
@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()方法传入了HttpServletRequestHttpServletResponse两个对象,分别代表HTTP请求和响应。

我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter,写入响应即可。

由于正确编写Servlet,需要清晰理解Java的多线程模型:

  • 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
  • HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
  • 在doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。

参考:Servlet开发

❓对于本节并不是很理解;

Servlet进阶

``
java
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    ...
}

HelloServlet能处理/hello这个路径的请求。

要处理GET、POST、PUT、DELETE等不同类型的请求,需要覆写doGet()doPost()doPut()等方法;当没有覆写时,

java
@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>

要注意axiosfetch API是无法在.then中直接处理重定向前的请求的,浏览器会根据新的地址直接重新发送请求,所以在.then中只能处理

servlet中,我们可以很容易的编写重定向的功能:

java
@WebServlet(urlPatterns = "/hi")
public class RedirectServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 构造重定向的路径:
        String redirectToUrl = "/hello";
        // 发送重定向响应:
        resp.sendRedirect(redirectToUrl);
    }
}

java
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("/hello").forward(req, resp);
    }
}

动手实现一个MVC框架

直接使用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>

  1. 通过普通的java类实现****<font style="color:rgb(51, 51, 51);">@GetMapping</font>****<font style="color:rgb(51, 51, 51);"> @PostMapping</font>****
java
public class UserController {
    @GetMapping("/signin")
    public ModelAndView signin() {
        ...
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(SignInBean bean) {
        ...
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        ...
    }
}
java
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}

动手 设计mvc框架

  • 构造一个DispatcherServlet作为一个控制中心,它总是映射到/,作为一个入口负责将所有请求的转发给对应controller
  • 扫描包中的controller注解,获取@GetMapping等注解指代的路径,构造Map<String url,dispatch>,用于路径的映射

实现

DispatchServlet是接受所有的请求的servelet,并根据controller中定义的path决定调用那个方法。作用架构如下图:

java
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
    private Map<String, GetDispatcher> getMappings = new HashMap<>();
    private Map<String, PostDispatcher> postMappings = new HashMap<>();
}
plain
class GetDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    String[] parameterNames; // 方法参数名称
    Class<?>[] parameterClasses; // 方法参数类型
}

<font style="color:rgb(32, 35, 39);">GetDispatcher</font>

java
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>

java
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();
    }
}
````````
java
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
        // ...
    }
    ...
}

````********
java
@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之后,整个请求的处理架构如下:

````
java
@WebFilter("/user/*")
public class AuthFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
       //......
    }
}

除了ServletFilter外,JavaEE的Servlet规范还提供了第三种组件:Listener

有几种Listener:

  • ServletContextListener:监听ServletContext的创建和销毁事件;
  • HttpSessionListener:监听HttpSession的创建和销毁事件;
  • ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
  • ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);
何标注为@WebListener,且实现了特定接口的类会被Web服务器自动初始化。我们编写一个实现了ServletContextListener接口的类如下:
java
@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.");
    }
}