Servlet入门


Servlet概念

Servlet(Server Applet),全称Java Servlet。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

最早支持Servlet标准的是JavaSoft的Java Web Server。此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。

Servlet生命周期

  1. Servlet 初始化后调用 init () 方法。

  2. Servlet 调用 service() 方法来处理客户端的请求。

  3. Servlet 销毁前调用 destroy() 方法。

  4. 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

init()方法

  • 服务器第一次被访问时,加载一个Servlet容器,即一个实例,而且只会被加载一次。

  • init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。

public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
}

Service()方法

  • service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

  • 每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用service()方法,而实例只有一个。

  • service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,一般来说只需要重写doGet、doPost等方法就行了。

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException
{
    
}

destroy()方法

  • destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

  • 在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。d

public void destroy() {
       
}

架构图

一个简单的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();
    }
}

在pom.xml文件中将打包类型设置为war,运行maven命令(可能要添加打包插件),将项目打包成.war文件,放入tomcat的webapps文件夹中,执行startup.bat文件启动tomcat服务器,tomcat会加载之前放入的.war文件,最后在浏览器中输入localhost:8080+对应的路径就可以访问了。

使用嵌入式Tomcat运行Servlet

上述方法不方便调试程序,我们可以自己写一个main方法来启动tomcat。

1.首先在pom.xml中添加相应的依赖

2.编写Servlet程序

@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        String name = req.getParameter("name");
        if (name == null) {
            name = "world";
        }
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, " + name + "!</h1>");
        pw.flush();
    }
}

3.编写main方法启动Tomcat服务器

public class Main {
    public static void main(String[] args) throws Exception {
        // 启动Tomcat:
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(Integer.getInteger("port", 8080));
        tomcat.getConnector();
        // 创建webapp:
        //通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp
        Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", 
        new File("target/classes").getAbsolutePath(), "/"));
        ctx.setResources(resources);
        tomcat.start();
        tomcat.getServer().await();
    }
}

在浏览器中输出localhost:8080/hello?name=qinfeng

浏览器显示:

映射路径

一个Web App就是由一个或多个Servlet组成的,每个Servlet通过注解说明自己能处理的路径。例如:

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

浏览器发出的HTTP请求总是由Web Server先接收,然后,根据Servlet配置的映射,不同的路径转发到不同的Servlet:

 

                            /hello    ┌───────────────┐
                          ┌──────────>│ HelloServlet  │
                          │           └───────────────┘
┌───────┐    ┌──────────┐ │ /signin   ┌───────────────┐
│Browser│───>│Dispatcher│─┼──────────>│ SignInServlet │
└───────┘    └──────────┘ │           └───────────────┘
                          │ /         ┌───────────────┐
                          └──────────>│ IndexServlet  │
                                      └───────────────┘
                              Web Server
               

这种根据路径转发的功能我们一般称为Dispatch。映射到/IndexServlet比较特殊,它实际上会接收所有未匹配的路径,相当于/*。

重定向

重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。

  • 302临时重定向

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

    浏览器发送get/hi请求,serlvet将处理这个请求,并发回302重定向响应,浏览器收到如下响应:

    HTTP/1.1 302 Found
    Location: /hello

    当浏览器收到响应,会根据Location的指示再发送get/hello请求。

    ┌───────┐   GET /hi     ┌───────────────┐
    │Browser│ ────────────> │RedirectServlet│
    │       │ <──────────── │               │
    └───────┘   302         └───────────────┘
  • 301永久重定向

    如果服务器发送301永久重定向响应,浏览器会缓存/hi到/hello这个重定向的关联,下次请求/hi的时候,浏览器就直接发送/hello请求了。

    resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
    resp.setHeader("Location", "/hello");

转发

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

@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //自己不做处理,将请求和响应转发给路径为/hello的servlet
        //注意:转发不会改动参数相当于再url中直接将/morning改为代码中的路径,即/hello
        //如果是代码中是"/hello?name=pop",将显示pop
        req.getRequestDispatcher("/hello").forward(req, resp);
    }
}

转发和重定向的区别在于:转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求。

 ┌────────────────────────┐————————————————————————
 |                        │      ┌───────────────┐ │
 |                        │ ────>│ForwardServlet │ │
┌───────┐  GET /morning   │      └───────────────┘ │
│Browser│ ──────────────> │              │         │
│       │ <────────────── │              ▼         │
└───────┘    200 <html>   │      ┌───────────────┐ │
                          │ <────│ HelloServlet  │ │
                          │      └───────────────┘ │
                          │       Web Server       │
                          └────────────────────────┘

Session

用户第一次访问服务器会自动获得一个Session ID,这个ID用于跟踪用户状态,用户在后续访问中总是携带此ID,但是如果一段时间内不访问,Session就会失效。在Servlet中调用getSession方法时,会自动创建一个Session ID,并用一个名为JSESSIONID的Cookie发送给浏览器,保存在客户端。

@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {

	private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123", "tom", "tomcat");

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/html");
		PrintWriter pw = resp.getWriter();
		pw.write("<h1>Sign In</h1>");
		pw.write("<form action=\"/signin\" method=\"post\">");
		pw.write("<p>Username: <input name=\"username\"></p>");
		pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
		pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>");
		pw.write("</form>");
		pw.flush();
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String name = req.getParameter("username");
		String password = req.getParameter("password");
		String expectedPassword = users.get(name.toLowerCase());
		if (expectedPassword != null && expectedPassword.equals(password)) {
			req.getSession().setAttribute("user", name);
			resp.sendRedirect("/");
		} else {
			resp.sendError(HttpServletResponse.SC_FORBIDDEN);
		}
	}
}

Cookie

Cookie,又称“小甜饼”。类型为“小型文本文件”,指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密。

Cookie除了发送Session ID,还可以用来设置用户偏好,比如记录用户选择的语言。

  • 创建:

    @WebServlet(urlPatterns = "/pref")
    public class LanguageServlet extends HttpServlet {
    
    	private static final Set<String> LANGUAGES = Set.of("en", "zh");
    
    	@Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    		String lang = req.getParameter("lang");
    		if (LANGUAGES.contains(lang)) {
    			Cookie cookie = new Cookie("lang", lang);
    			cookie.setPath("/");
    			cookie.setMaxAge(8640000); // 100 days
    			resp.addCookie(cookie);
    		}
    		resp.sendRedirect("/");
    	}
    }

    创建一个新Cookie时,除了指定名称和值以外,通常需要设置setPath(“/“),浏览器根据此前缀决定是否发送Cookie。如果一个Cookie调用了setPath(“/user/“),那么浏览器只有在请求以/user/开头的路径时才会附加此Cookie。通过setMaxAge()设置Cookie的有效期,单位为秒,最后通过resp.addCookie()把它添加到响应。

    如果访问的是https网页,还需要调用setSecure(true),否则浏览器不会发送该Cookie。反正,如果设置了Secure,就必须以https来访问。

  • 读取:

    private String parseLanguageFromCookie(HttpServletRequest req) {
        // 获取请求附带的所有Cookie:
        Cookie[] cookies = req.getCookies();
        // 如果获取到Cookie:
        if (cookies != null) {
            // 循环每个Cookie:
            for (Cookie cookie : cookies) {
                // 如果Cookie名称为lang:
                if (cookie.getName().equals("lang")) {
                    // 返回Cookie的值:
                    return cookie.getValue();
                }
            }
        }
        // 返回默认值:
        return "en";
    }

    可见,读取Cookie主要依靠遍历HttpServletRequest附带的所有Cookie。

JSP

用PrintWriter输出HTML比较痛苦,因为不但要正确编写HTML,还需要插入各种变量。我们可以使用JSP。

JSP是Java Server Pages的缩写,它的文件必须放到/src/main/webapp下,文件名必须以.jsp结尾,整个文件与HTML并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令<% … %>。

<html>
<head>
    <title>Hello World - JSP</title>
</head>
<body>
    <%-- JSP Comment --%>
    <h1>Hello World!</h1>
    <p>
    <%
         out.println("Your IP address is ");
    %>
    <span style="color:red">
        <%= request.getRemoteAddr() %>
    </span>
    </p>
</body>
</html>
  • 特殊指令<%…%>:

    • 包含在<%–和–%>之间的是JSP的注释,它们会被完全忽略;
    • 包含在<%和%>之间的是Java代码,可以编写任意Java代码;
    • 如果使用<%= xxx %>则可以快捷输出一个变量的值。
  • JSP页面内置了几个变量:

    • out:表示HttpServletResponse的PrintWriter;
    • session:表示当前HttpSession对象
    • request:表示HttpServletRequest对象。
  • JSP与Servlet:

    JSP在执行前首先被编译成一个Servlet。在Tomcat的临时目录下,可以找到一个hello_jsp.java的源文件。

    package org.apache.jsp;
    import ...
    
    public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
        implements org.apache.jasper.runtime.JspSourceDependent,
                   org.apache.jasper.runtime.JspSourceImports {
    
        ...
    
        public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
            throws java.io.IOException, javax.servlet.ServletException {
            ...
            out.write("<html>\n");
            out.write("<head>\n");
            out.write("    <title>Hello World - JSP</title>\n");
            out.write("</head>\n");
            out.write("<body>\n");
            ...
        }
        ...
    }

    可见JSP本质上就是一个Servlet,只不过无需配置映射路径,Web Server会根据路径查找对应的.jsp文件,如果找到了,就自动编译成Servlet再执行。在服务器运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。

JSP高级功能

  • 通过page指令引入Java类:

    <%@ page import="java.io.*" %>
    <%@ page import="java.util.*" %>
  • 使用include指令可以引入另一个JSP文件:

    <html>
    <body>
        <%@ include file="header.jsp"%>
        <h1>Index Page</h1>
        <%@ include file="footer.jsp"%>
    </body>

一个简单的MVC模型

  • model:

    public class User {
    
    	public long id;
    	public String name;
    	public School school;
    
    	public User(long id, String name, School school) {
    		this.id = id;
    		this.name = name;
    		this.school = school;
    	}
    }
    public class School {
    
    	public String name;
    	public String address;
    
    	public School(String name, String address) {
    		this.name = name;
    		this.address = address;
    	}
    }
    
  • controller:

    public class UserServlet extends HttpServlet {
    
    	@Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    		School school = new School("No.1 Middle School", "101 North Street");
    		User user = new User(123, "Bob", school);
    		req.setAttribute("user", user);
    		req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
    	}
    }
  • view:

    <%
        User user = (User) request.getAttribute("user");
    %>
    <html>
    <head>
        <title>Hello World - JSP</title>
    </head>
    <body>
        <h1>Hello <%= user.name %>!</h1>
        <p>School Name:
        <span style="color:red">
            <%= user.school.name %>
        </span>
        </p>
        <p>School Address:
        <span style="color:red">
            <%= user.school.address %>
        </span>
        </p>
    </body>
    </html>

使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。

Filter

Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起。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。这里的/*表示所有路径。

            ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
            |s                      /             ┌──────────────┐
            │                     ┌─────────────>│ IndexServlet │ │
                                  │              └──────────────┘
            │                     │/signin       ┌──────────────┐ │
                                  ├─────────────>│SignInServlet │
            │                     │              └──────────────┘ │
                                  │/signout      ┌──────────────┐
┌───────┐   │   ┌──────────────┐  ├─────────────>│SignOutServlet│ │
│Browser│──────>│EncodingFilter├──┤              └──────────────┘
└───────┘   │   └──────────────┘  │/user/profile ┌──────────────┐ │
                                  ├─────────────>│ProfileServlet│
            │                     │              └──────────────┘ │
                                  │/user/post    ┌──────────────┐
            │                     ├─────────────>│ PostServlet  │ │
                                  │              └──────────────┘
            │                     │/user/reply   ┌──────────────┐ │
                                  └─────────────>│ ReplyServlet │
            │                                    └──────────────┘ │
             ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─

注意

  • Servlet规范并没有对@WebFilter注解标注的Filter规定顺序。如果一定要给每个Filter指定顺序,就必须在web.xml文件中对这些Filter再配置一遍。
  • 如果Filter要使请求继续被处理,就一定要调用chain.doFilter()!

文章作者: 淡夜
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 淡夜 !
评论
  目录