JavaWeb基础(五)-Servlet详解

作者: sixleaves | 来源:发表于2018-08-17 20:14 被阅读59次

    这篇文章, 我主要分享JavaWeb动态网页开发.要谈JavaWeb动态网页开发.我们就要来了解Servlet, 所以我会按如下进行分享

    • Servlet的规范
    • Servlet的请求流程
    • Servlet初始化参数
    • Servlet继承体系的设计原因
    • Servlet请求和响应
      主要围绕Servlet的原理Servlet的应用两条主线来分享.
      Servlet的原理: 包含Servlet的规范Servlet的基本请求流程Servlet继承体系的设计原因
      Servlet的应用: 包含Servlet初始化参数Servlet请求和响应

    1.什么是Servlet规范

    Servlet是一套规范, 所谓的规范就是做了约束.

    • 是一个规范.Servlet是一个规范,所谓的规范就是Servlet约束了一些东西, 主要约束了容器、约束了项目目录结构约束了我们的代码.
    • 是一个组件. Servlet也可以看成是一个组件, 因为一个遵循Servlet规范的程序可以跑在实现Servlet规范的容器中。
    Servlet的规范

    2.如何编写一个最简单的Servlet的步骤

    • 创建一个HelloServlet类继承自HttpServlet.。
    • 复写其service方法, 在其中编写业务逻辑处理请求。
    • 配置web.xml. 增加xml中的servlet对象和servlet-mapping对象.其中servlet对象用来关联我们的类,并给这个类一个别名。servlet-mapping对象用来映射servlet和和访问的资源名。这样才来告诉tomcat容器, 一旦请求来了, 该根据资源名去找哪个servlet做处理。

    3.如何研究Servlet对象的生命周期

    Servlet对象生命周期依赖于容器, 因为Servlet规范已经约定了Servlet对象必须放在实现Servlet规范的容器中运行, 换区话说Servlet对象从出生到死亡都由容器调度。所以容器负责Servlet的创建、初始化、运行、释放.

    要了解Servlet具体的生命周期, 我们先介绍以下上个方法.这三个方法是Servlet的生命周期方法.

    生命周期方法

    • init(ServletConfig); 负责初始化Servlet.
    • service(ServletRequest, ServletResponse); 负责处理请求.
    • destory(); 负责释放Servlet.

    观察Servlet的生命周期

    我们可以创建一个HelloServlet如下, 并且给它提供一个无参构造方法, 分别在上面上个生命周期方法中做打印, 来研究其调用顺序.

    创建HelloServlet, 继承自HttpServlet.复写生命周期方法

    package com.sweetcs.web.servlet;
    
    import java.io.IOException;
    
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class HelloServlet extends HttpServlet{
        
        public HelloServlet() {
            // TODO Auto-generated constructor stub
            System.out.println("Servlet构造方法");
        }
        
        @Override
        public void init(ServletConfig config) throws ServletException {
            // TODO Auto-generated method stub
            super.init(config);
            
            System.out.println("Servlet init");
        }
        
        
        @Override
        protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
            System.out.println("Servlet service");
        }
        
        @Override
        public void destroy() {
            // TODO Auto-generated method stub
            super.destroy();
            System.out.println("Servlet destroy");
        }   
    }
    

    web.xml中配置资源名的要交个哪个servlet处理

    <?xml version="1.0" encoding="UTF-8"?>
    
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0"
      metadata-complete="true">
      <display-name>Welcome to Tomcat</display-name>
      <description>
         Welcome to Tomcat
      </description>
    
    
        <servlet>
            <servlet-name>HelloServlet</servlet-name>
            <servlet-class>com.sweetcs.web.servlet.HelloServlet</servlet-class>
        </servlet>
        
        <servlet-mapping>
            <servlet-name>HelloServlet</servlet-name>
            <url-pattern>/hello</url-pattern>
        </servlet-mapping>
      
    </web-app>
    
    

    第一次访问http://127.0.0.1:8080/hello打印输出

    第一次访问

    第二、三次访问http://127.0.0.1:8080/hello打印输出

    第二、三次访问

    通过上述观察可以发现,第一次访问是依次调用了构造方法init方法service方法.但是什么时候Servlet对象的destory方法才会调用呢?我们进行以下两种方式验证。

    • 第一种停止tomcat的运行, 点击带有叉号的猫
    点击带有叉号的猫

    输出如下,说明调用了destory方法, Servlet被正确的释放掉.

    打印出destory
    • 第二种直接终止程序.
    点击小红点终止程序

    输出如下, 说明Servlet对象的destory方法没有被容器调用, 也就是说Servlet没有被正确的释放掉.

    根据上述实验验证,我们可以得出以下结论

    总结

    • Servlet的生命周期是先由容器创建Servlet对象, 所以会调用其构造方法.然后再调用init方法进行初始化, 最后调用service方法处理请求.
    • 由次可以见Servlet只在第一次访问时候创建.创建完后容器就调用了servlet的init方法.
    • Servlet对象在整个容器中只有一份,属于单例.因为多次的相同url的请求过来, 都没有再调用构造方法, 显然没有在创建新的Sevlet对象.
    • Servlet必须提供无参数构造器.由于Sevlet对象不是我们手动创建的, 而是交给容器负责。在这里我们使用的是tomcat容器.而tomcat容器是使用反射技术Class clazz = Class.forName();clazz.newInstance()创建Servlet对象.所以我们必须提供无参构造器.当然Java中如果你不提供构造器, 默认就会存在一个无参构造器。
    • 要正确的让Servlet得到释放, 必须得停止tomcat容器.如果是程序异常中断, Servlet并不能得到释放.

    4.Servlet请求流程

    我们了解了Servlet的规范、Servlet的生命周期、接着我们研究下我们从浏览器通过http协议请求相应的资源的时候。服务器端的Servlet从接受到请求的到响应的流程.

    • 1.当tomcat服务器接受到浏览器的请求时, 会进行URL的解析.将URL分解出ContextPathResourceName.即上下文路径资源名称.
    • 2.解析完成后, tomcat会去读取其conf/server.xml中的所有Context对象.(我把Context元素叫对象)
    • 3.然后根据ContextPath的值和Context对象path属性进行比较, 找到对应的Context对象。
      如果没有找到对应的Context对象就报404 Not Found.
      如果有找到对应的Context对象则进入第4步.(如下是一个Context元素<Context docBase='xxx' path='contextPath' />)
    • 4.读取Context对象的docBase属性值,即为项目的部署路径所在地址.tomcat会根据该地址取读取其下的WEB-INF/web.xml配置文件.
    • 5.解析web.xml中的所有Servlet-Mapping对象, 并用ResouceName和Servlet-Mapping对象的url-pattern比较.如果知道到匹配的进入第6步, 否者也是报404 Not Found.
    • 6.根据找到的Servlet-Mapping对象的servlet-name继续找到Servlet对象对应的全限定类名.
    • 7.尝试去 Servlet缓存池获取对应的Servlet对象.根据全限定类名取对应的Servlet对象.如果取到, 则直接调用其service方法(传入已经创建好的request和reponse对象)
    • 8.如果Servlet缓存池中没有对应的对象, 则使用反射技术, 加载解析出来的全限定类名.并调用其newInstance()方法创建Servlet对象.
    • 9.创建ServletConfig对象, 该对象即代表web.xml.调用Servlet对象的init方法.servlet.init(config)
      1. 调用Servlet对象的service方法处理请求, 相应数据. servlet.service(request, response)
      1. 如果有重复的请求进来, 则进入第7步.

    注意

    • server.xml文件配置了contextPath.web.xml配置了资源名称.
    • server.xml文件让tomcat根据contextPath去找到对应的根目录地址.web.xml文件根据tomcat根据资源名称知道对应的Servlet类,再通过反射技术创建Sevlet对象.

    5.Servlet初始化参数

    了解完Servlet的请求流程, 我们也就明白了底层是如何创建Servlet对象, 同时知道了配置文件的作用。接着我们就顺着该流程的第一个步骤, 创建并初始化Servlet对象, 想来了解下Servlet的初始化参数.

    5.1 init(config)

    什么是初始化参数, 即Servlet在创建后, 我们可能会需要它附带一些属性, 并且我们用配置好的参数来初始化这些Servlet.这些参数都是配置在web.xml<Servlet元素>中.
    如下我们给HelloServlet配置初始化参数, 并读取出来查看.

    web.xml配置, 在servlet元素中添加init-param元素

        <servlet>
            <servlet-name>HelloServlet</servlet-name>
            <servlet-class>com.sweetcs.web.servlet.HelloServlet</servlet-class>
            <init-param>
                <param-name>encoding</param-name>
                <param-value>UTF-8</param-value>
            </init-param>
        </servlet>
    

    Java代码

    public class HelloServlet extends HttpServlet{
        
        private ServletConfig config;
        
        public HelloServlet() {
            // TODO Auto-generated constructor stub
            System.out.println("Servlet构造方法");
        }
        
        @Override
        public void init(ServletConfig config) throws ServletException {
            // TODO Auto-generated method stub
            super.init(config);
            this.config = config;
            System.out.println("Servlet init");
        }
        
        
        @Override
        protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
            System.out.println("Servlet service");
            
            String encoding = config.getInitParameter("encoding");
            System.out.println(encoding);
            
        }
        
        @Override
        public void destroy() {
            // TODO Auto-generated method stub
            super.destroy();
            System.out.println("Servlet destroy");
        }   
    }
    

    启动tomcat, 并浏览器输入http://127.0.0.1:8080/hello.打印如下,可以正确的将配置参数读取出来.

    Servlet构造方法
    Servlet init
    Servlet service
    UTF-8
    

    5.2

    根据5.1我们知道一个ServletConfig对象 对应的就是其WEB-INF下的web.xml配置文件.其主要作用就是用来读取配置文件中的Servlet元素的初始化信息

    其有以下两个常用接口

    • public String getInitParameter(String name); 根据配置的参数的key读取对应的value值
    • public Enumeration<String> getInitParameterNames(); 可以读取到所有的配置参数的key值.

    6 Servlet继承体系的设计原因

    上面我们分享了如何编写一个Servlet程序、以及Servlet的生命周期过程、Servlet的请求流程和Servlet的初始化参数。接着我们就来研究下Servlet的继承体系。我主要借助eclipse和源码来和大家分享下Servlet的继承体系的结构和设计原因.

    1.Servlet的是一个规范, 我们如何遵循其规范来编写程序呢?

    我们通过查看Servlet接口可以看到其对应的接口代码.如果让我们自己来实现一个Servlet程序,那么我们首先得实现其Servlet接口.
    并且我们得实现其生命周期方法, 来让tomcat能管理Servlet对象、并且我们得实现ServletConfig接口.让Servlet能得到正确的初始化.依照这个思路我们可以写出第一个版本实现Servlet接口.

    package com.sweetcs.web.servlet;
    
    import java.io.IOException;
    
    import javax.security.auth.login.Configuration;
    import javax.servlet.Servlet;
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    
    public class MyServlet implements Servlet{
    
        private ServletConfig config;
        
        @Override
        public void init(ServletConfig config) throws ServletException {
            // TODO Auto-generated method stub
            System.out.println("初始化参数");
            this.config = config;
        }
    
        @Override
        public ServletConfig getServletConfig() {
            // TODO Auto-generated method stub
            return this.config;
        }
    
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            // TODO Auto-generated method stub
            
            System.out.println("处理请求");
        }
    
        @Override
        public String getServletInfo() {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public void destroy() {
            // TODO Auto-generated method stub
            System.out.println("desotry servlet");
        
        }
        
    }
    
    

    为了让tomcat能正确找到对应的Servlet我们还需要配置web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0"
      metadata-complete="true">
      <display-name>Welcome to Tomcat</display-name>
      <description>
         Welcome to Tomcat
      </description>
    
        <servlet>
            <servlet-name>MyServlet</servlet-name>
            <servlet-class>com.sweetcs.web.servlet.MyServlet</servlet-class>
        </servlet>
        
        <servlet-mapping>
            <servlet-name>MyServlet</servlet-name>
            <url-pattern>/test</url-pattern>
        </servlet-mapping>
      
    </web-app>
    
    

    启动tomcat.并在浏览器中输入http://127.0.0.1:8080/test进行测试.

    初始化参数
    处理请求
    

    2.如何读取初始化参数

    我们现在遵循Servlet接口开发的Servlet程序能正常运行了,但是我们如何读取配置呢?我们可以这样做,用一个ServletConfig属性存储该ServletConfig对象, 在service方法需要的时候进行读取.我们在service方法中读取
    如下代码

        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            // TODO Auto-generated method stub
            
            System.out.println("处理请求");
            String encoding = config.getInitParameter("encoding");
        }
    

    为什么上述实现需要重构
    上述代码并不存在问题, 但是如果我们现在有10个Servlet要编写, 我们就得重复的在每个Servlet中存储一个ServletConfig属性用来存储ServletConfig.而且我们还得为每个Servlet实现上述代码中的所有接口。这时候代码就重复了, 一旦代码重复, 我们就要考虑进行重构,如何进行重构呢?对于重复代码我们可以采用继承方式或者组合方法.一般我们都使用组合方式进行重构, 但是在这里由于我们编写的Servlet都是必须实现该规范的, 所以我们并没必要去用组合的方式.
    由此思路我们可以将这些代码抽取到一个类中, 我们把它叫MyGenericServlet

    3.消除Servlet接口重复代码,实现MyGenericServlet

    package com.sweetcs.web.servlet;
    
    import java.io.IOException;
    
    import javax.servlet.Servlet;
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    
    public abstract class MyGenericServlet implements Servlet{
        private ServletConfig config;
        
        @Override
        public void init(ServletConfig config) throws ServletException {
            // TODO Auto-generated method stub
            System.out.println("初始化参数");
            this.config = config;
        }
    
        @Override
        public ServletConfig getServletConfig() {
            // TODO Auto-generated method stub
            return this.config;
        }
    
        @Override
        public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
        
    
        @Override
        public String getServletInfo() {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public void destroy() {
            // TODO Auto-generated method stub
            System.out.println("desotry servlet");
        
        }
    }
    
    • 之所以将service声明为抽象方法,是因为如果不这样做.这很容易忘记要复写该service方法,该方法主要负责请求的处理, 不同的servlet对请求的处理逻辑是不尽相同的, 所以将其声明为抽象方法是比较合适的.
    • 根据抽象方法约定, MyGenericServlet也必须为抽象类

    4.现在考虑子类如何读取初始化参数?

    • 首先子类并没有办法直接访问父类的config属性,因为该属性是private的, 比较好的方法是父类提供进一步的封装让我们能根据key来读取初始化参数值.
    • 考虑到ServletConfig对象中拥有两个常用方法,我们可以在父类中实现这两个常用方法, 而Servlet规范中,恰好也提供了ServletConfig接口,所以我们可以让MyGenericServlet实现该接口.
    public abstract class MyGenericServlet implements Servlet, ServletConfig{
        private ServletConfig config;
        
        @Override
        public String getInitParameter(String name) {
            // TODO Auto-generated method stub
            return config.getInitParameter(name);
        }
        
        @Override
        public Enumeration<String> getInitParameterNames() {
            // TODO Auto-generated method stub
            return config.getInitParameterNames();
        }
    .......// 省略后面代码
    }
    

    5.需要定制专门处理Http的Servlet

    经过1--4 我们已经抽取了MyGenericServlet, 进一步让代码重用性得到提高.现在的问题是, MyGenericServlet是处理一般的资源的Servlet。其service方法中的request和response对象并不是HttpRequest和HttpResponse对象.也就是说MyGenericServlet并不适合处理Http类型的请求,它是一个处理通用的请求,子类还需要继承MyGenericServlet做进一步订制.

    • 我们需要实现MyHttpServlet,让其继承MyGenericServlet.并复写其service方法.在该方法中我们对ServletRequest和ServletResponse进行强制类型转换.并提供一个重载的service方法。在该重载service方法中我们进行方法的分发。具体代码如下
    package com.sweetcs.web.servlet;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class MyHttpServlet extends MyGenericServlet{
    
        
        @Override
        public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
    
            HttpServletRequest  request;
            HttpServletResponse response;
    
            try {
                request = (HttpServletRequest) req;
                response = (HttpServletResponse) res;
            } catch (ClassCastException e) {
                throw new ServletException("non-HTTP request or response");
            }
            service(request, response);
        }
        
        protected void service(HttpServletRequest req, HttpServletResponse resp) {
            
            String method = req.getMethod();
            
            if ("GET".equalsIgnoreCase(method)) {
                doGet();
            }else if ("POST".equalsIgnoreCase(method)) {
                doPost();
            }else {
                
            }
            
        }
    
        private void doPost() {
            // TODO Auto-generated method stub
            
        }
    
        private void doGet() {
            // TODO Auto-generated method stub
            doPost();
        }
    }
    

    继承体系图

    通过上述代码,我们可以可以对比下我们实现的继承体系HttpServlet的继承体系。(按command + t查看类的继承结构)
    其之所以这样设计,就是为了消除代码重复,为了提供专门处理Http请求的Servlet

    MyHttpServlet的继承结构
    HttpServlet继承结构
    总结Servlet继承结构
    • GenericServlet是一个抽象类, 实现了Sevlet接口, 并实现了大多数的Servlet接口方法, 并实现了ServletConfig接口用来读取初始化参数.(还实现了java.io.Serializable接口支持网络序列化传输)
    • HttpServlet继承自GeneircServlet是对其进一步的扩展,专门用来处理Http类型的请求.

    7. Servlet请求和相应

    我们了解了整个Servlet的请求流程和其体系结构,其实现在我们就可以进行更多复杂的业务逻辑开发。在这之前我们需要了了解下HttpServletRequest和HttpServletResponse的常用API.
    由于是API的使用, 我们将简要的介绍完API功能后, 使用代码演示一个注册用户的功能.

    HttpServletRequest

    是什么:HttpServletRequest是ServletRequest的子类, 主要用于处理Http请求, 其表示一个Http请求对象.

    常用API
    • getMethod(). 获取请求对象的方式,是GET还是POST等等
    • getRequestURI(). 获取请求的资源路径(contextPath+resouceName)
    • getRequestURL(). 获取整个URL地址
    • getHeader(String name) 根据name获取请求头中的字段信息.
    获取请求参数的方法
    • getParameter(String name);
    • getParameterValues(String name);
    • getParameterNames();
    • getParameterMap();
    request的编码问题
    • 如果是post请求, 记得设置为UTF-8编码 req.setCharacterEncoding("UTF-8");
    • 如果是GET请求, 可以先进行转码, 先转成字节数组,再转换成UTF-8格式的字符编码(tomcat中请求对象,默认是ISO-8859-1编码,不兼容中文.)
    byte[] data = username.getBytes("ISO-8859-1");
    username = new String(data, "UTF-8"); // 这时候就可以正确的输出
    

    HttpServletResponse

    常用API
    • getOuputStream(); 获取字节输出流.常用在文件的下载
    • getWriter(); 获取字符输出流.用在像网页输出字符
    注意
    • 记得设置MIME类型和charset.
      resp.setContentType("text/html; charset=UTF-8")

    需求,写代码

    我们需要完成这样一个需求, 通过servlet完成一个网页版的计算器。具体页面如下, 因为我们还没介绍jsp, 所以需要在servlet中完成网页页面的绘制.

    页面

    计算器页面

    代码

    package com.sweetcs.calc.servlet;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @WebServlet("/calc")
    public class CalcServlet extends HttpServlet{
    
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            
            req.setCharacterEncoding("UTF-8"); // 只针对post请求有效
            resp.setContentType("text/html; charset=UTF-8"); // 设置输出的类型和编码
            String op = req.getParameter("op");
            String leftNum =  req.getParameter("leftNum");
            String rightNum = req.getParameter("rightNum");
            
            if (leftNum == null || "".equals(leftNum)) leftNum = "0";
            if (rightNum == null || "".equals(rightNum)) rightNum = "0";
            
            String result = "0";
            System.out.println(op);
            System.out.println(leftNum + "," + rightNum);
            
            
            
            if (op != null) {
                switch (op) {
                case "+":
                    result = (Double.parseDouble(leftNum) + Double.parseDouble(rightNum))+"";
                    break;
                case "-":
                    result =  (Double.parseDouble(leftNum) - Double.parseDouble(rightNum))+"";
                    break;
                case "*":
                    result =  (Double.parseDouble(leftNum) * Double.parseDouble(rightNum))+"";
                    break;
                case "/":
                    result =  (Double.parseDouble(leftNum) / Double.parseDouble(rightNum))+"";
                    break;
                default:
                    break;
                }
            }
            
            PrintWriter pw = resp.getWriter();
            pw.write("<!DOCTYPE html> <html>"
                    + " <head> "
                    + "<meta charset='UTF-8'> "
                            + "<title>Insert title here</title> "
                            + "</head> "
                            + "<body> " + "<form action='/calc' method='GET'>"
                            + "<input type='text' name='leftNum' /> "
                            + "<select name = 'op'> "
                            + "<option value='+'>+</option> "
                            + "<option value='-'>-</option> "
                            + "<option value='*'>*</option> "
                            + "<option value='/'>/</option> "
                            + "</select> "
                            + "<input type='text' name='rightNum' /> "
                            + "<input type='submit' value='=' /> "
                                    + "<input type='text' name='result' value="+ result +" /> "
                                    + "</form> "
                                    + "</body> "
                                    + "</html>");
            pw.close();
            
        }
        
    }
    

    后续

    JavaWeb基础(六)中我将分享Web开发中的Cookie和Session.

    相关文章

      网友评论

        本文标题:JavaWeb基础(五)-Servlet详解

        本文链接:https://www.haomeiwen.com/subject/oduobftx.html