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,写入响应即可。

由于Web服务器会使用多线程的方式执行Servlet请求,因此要正确编写Servlet,需要清晰理解Java的多线程模型:

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

启动嵌入式Tomcat并加载当前工程的webapp

参考:Servlet开发 ❓对于本节并不是很理解;

Servlet进阶

每个Servlet通过注解说明自己能处理的路径。例如:

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

HelloServlet能处理/hello这个路径的请求。 要处理GET、POST、PUT、DELETE等不同类型的请求,需要覆写doGet()doPost()doPut()等方法;当没有覆写时,会直接返回405或400错误。

java
@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协议中,重定向是一种常见的机制,服务器可以通过返回特定的状态码(如301302)来告诉客户端需要跳转到另一个URL。

  • 301 Moved Permanently:表示请求的资源已经永久移动到了新的地址。
  • 302 Found:表示请求的资源暂时性地移动到了一个新的地址。

浏览器遇到重定向时的处理过程通常如下:

  1. 接收到重定向响应后,浏览器根据重定向状态码(301或302)决定是否跳转。
  2. 对于301,浏览器会直接跳转到新的URL,并且将新URL加入浏览器历史记录中。
  3. 对于302,浏览器会发送新的请求到重定向的地址,获取重定向后的内容,并更新地址栏显示新URL。

要注意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);
    }
}

转发

当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。

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);
    }
}

使用servlet实现一个简易mvc框架

动手实现一个MVC框架

Servlet提供的接口仍然偏底层,直接使用servelt编写的java web应用会导致业务代码与网络请求的代码耦合在一起,不利于开发。所以能不能直接使用普通的java类来编写业务逻辑? MVC(Model-View-Controller)模式是一种软件设计模式,常用于web应用程序的开发,将应用程序分解为三个主要部分:模型(Model)视图(View)控制器(Controller),从而实现了数据、业务逻辑与视图层的松耦合。

  • 模型(Model):代表应用程序的数据结构、业务逻辑和状态。
  • 视图(View):负责展示数据
  • **控制器:**充当模型和视图之间的中介,将用户的请求转发给模型进行处理,并将处理结果返回给视图显示给用户。

在Java中,Spring MVC就是基于MVC模式构建的,而Spring MVC的底层是基于Servlet实现的,我们来看看它的核心开发思想和原理。

mvc框架的基本特性

  1. 通过普通的java类实现Controller,同时对于不同的请求方法(get、post),通过**@GetMapping**** @PostMapping**等注解标注的方法进行处理。
java
public class UserController {
    @GetMapping("/signin")
    public ModelAndView signin() {
        ...
    }

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

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        ...
    }
}
  1. 对于HttpServletRequest、HttpServletResponse、HttpSession,只要方法参数有定义,就可以自动传入:
java
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}

动手 设计mvc框架

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

实现DispatcherServlet

DispatchServlet是接受所有的请求的servelet,并根据controller中定义的path决定调用那个方法。作用架构如下图: image.png 首先,我们需要存储请求路径到某个具体方法的映射:

java
@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方法处理真正的请求:

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;
    }
}

postDispatch的实现基本相同。 接下来,我们来实现整个DispatcherServlet的处理流程,以doGet()为例:

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();
    }
}

最后一步是在DispatcherServletinit()方法中初始化所有GetPost的映射,以及用于渲染的模板引擎:

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
        // ...
    }
    ...
}

filter

JavaEE的Servlet规范还提供了一种Filter组件,即过滤器,它的作用是,在HTTP请求到达Servlet之前,可以被一个或多个Filter预处理。类似打印日志、登录检查等逻辑,完全可以放到Filter中。 我们编写一个最简单的EncodingFilter,它强制把输入和输出的编码设置为UTF-8:

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之后,整个请求的处理架构如下: image.png 除了使用/*处理所有路径的请求,也可以编写特定路径进行过滤的Filter,比如说下边这段代码可以只过滤以/user/开头的路径。

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

listen

除了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.");
    }
}