01-Servlet

一、理解Http

1、定义

​ HTTP是基于TCP协议的。TCP负责数据传输,而HTTP只是规范了TCP传输的数据的格式。

2、socket编程实现HTTP服务的底层。

class SocketHandler implements Runnable {
    final static String CRLF = "\r\n";   // 1
    private Socket clientSocket;

    public SocketHandler(Socket clientSocket) {
        this.clientSocket = clientSocket;
    }

    public void handleSocket(Socket clientSocket) throws IOException {

        BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        PrintWriter out = new PrintWriter(new BufferedWriter( new OutputStreamWriter(clientSocket.getOutputStream())),true);

        String requestHeader = "";
        String s;
        while ((s = in.readLine()) != null) {
            s += CRLF;  // 2 很重要,默认情况下in.readLine的结果中`\r\n`被去掉了
            requestHeader = requestHeader + s;
            if (s.equals(CRLF)){ // 3 此处HTTP请求头我们都得到了;如果从请求头中判断有请求正文,则还需要继续获取数据
                break;
            }
        }
        System.out.println("客户端请求头:");
        System.out.println(requestHeader);

        String responseBody = "客户端的请求头是:\n"+requestHeader;

        String responseHeader = "HTTP/1.0 200 OK\r\n" +
                "Content-Type: text/plain; charset=UTF-8\r\n" +
                "Content-Length: "+responseBody.getBytes().length+"\r\n" +
                "\r\n";
        // 4 问题来了:1、浏览器如何探测编码 2、浏览器受到content-length后会按照什么方式判断?汉字的个数?字节数?

        System.out.println("响应头:");
        System.out.println(responseHeader);

        out.write(responseHeader);
        out.write(responseBody);
        out.flush();

        out.close();
        in.close();
        clientSocket.close();

    }

    @Override
    public void run() {
        try {
            handleSocket(clientSocket);
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

}

public class MyHTTPServer {

    public static void main(String[] args) throws Exception {

        int port = 8000;
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("启动服务,绑定端口: " + port);

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(30);  // 5

        while (true) {  // 6
            Socket clientSocket = serverSocket.accept();
            System.out.println("新的连接"
                    + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
            try {
                fixedThreadPool.execute(new SocketHandler(clientSocket));
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
}

二、Servlet

2.1、Servlet简介

  Servlet是sun公司提供的一门用于开发动态web资源的技术。
  Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向浏览器输出数据),需要完成以下2个步骤:
  1、编写一个Java类,实现servlet接口。
  2、把开发好的Java类部署到web服务器中。
  按照一种约定俗成的称呼习惯,通常我们也把实现了servlet接口的java程序,称之为Servlet

2.2、Servlet的运行过程

Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:
  ①Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
  ②装载并创建该Servlet的一个实例对象。
  ③调用Servlet实例对象的init()方法。
  ④创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
  ⑤WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。

2.3、Servlet与普通Java类的区别

  Servlet是一个供其他Java程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。
  针对客户端的多次Servlet请求,通常情况下,服务器只会创建一个Servlet实例对象,也就是说Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,servlet实例对象才会销毁。
  在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。

  如果在元素中配置了一个元素,那么WEB应用程序在启动时,就会装载并创建Servlet的实例对象、以及调用Servlet实例对象的init()方法。

   <servlet>
     <servlet-name>invoker</servlet-name>
     <servlet-class>
       org.apache.catalina.servlets.InvokerServlet
     </servlet-class>
     <load-on-startup>1</load-on-startup>
   </servlet>

  用途:为web应用写一个InitServlet,这个servlet配置为启动时装载,为整个web应用创建必要的数据库表和数据。

2.4、Servlet接口实现类

  Servlet接口SUN公司定义了两个默认实现类,分别为:GenericServlet、HttpServlet。

  HttpServlet指能够处理HTTP请求的servlet,它在原有Servlet接口上添加了一些与HTTP协议处理方法,它比Servlet接口的功能更为强大。因此开发人员在编写Servlet时,通常应继承这个类,而避免直接去实现Servlet接口。
  HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用HttpServlet的doGet方法,如为Post请求,则调用doPost方法。因此,开发人员在编写Servlet时,通常只需要覆写doGet或doPost方法,而不要去覆写service方法。

public class ServletDemo1 extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
       response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
        out.println("<HTML>");
        out.println("  <HEAD><TITLE>A Servlet</TITLE></HEAD>");
        out.println("  <BODY>");
        out.print("    This is ");
        out.print(this.getClass());
        out.println(", using the POST method");
        out.println("  </BODY>");
        out.println("</HTML>");
        out.flush();
        out.close();
    }
}

2.5、Servlet访问URL映射配置

​ 由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用元素和元素完成。
  元素用于注册Servlet,它包含有两个主要的子元素:,分别用于设置Servlet的注册名称和Servlet的完整类名。
元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素:,分别用于指定Servlet的注册名称和Servlet的对外访问路径。

​ 同一个Servlet可以被映射到多个URL上,即多个元素的子元素的设置值可以是同一个Servlet的注册名。

示例:

<servlet>
	<servlet-name>ServletDemo1</servlet-name>
	<servlet-class>gacl.servlet.study.ServletDemo1</servlet-class>
</servlet>

<servlet-mapping>
	<servlet-name>ServletDemo1</servlet-name>
	<url-pattern>/servlet/1</url-pattern>
    <url-pattern>/servlet/2</url-pattern>
</servlet-mapping>

2.6、Servlet的线程安全问题

​ 当多个客户端并发访问同一个Servlet时,web服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用Servlet的service方法,因此service方法内如果访问了同一个资源的话,就有可能引发线程安全问题。

示例:

public class ServletDemo3 extends HttpServlet {
    int i=1;
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        i++;
        try {
            Thread.sleep(1000*4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        response.getWriter().write(i+"");
    }
}
/*
把i定义成全局变量,当多个线程并发访问变量i时,就会存在线程安全问题了,如下图所示:同时开启两个浏览器模拟并发访问同一个Servlet,本来正常来说,第一个浏览器应该看到2,而第二个浏览器应该看到3的,结果两个浏览器都看到了3,这就不正常。
*/

三、ServletConfig

3.1、配置Servlet初始化参数

​ 在Servlet的配置文件web.xml中,可以使用一个或多个标签为servlet配置一些初始化参数。

<servlet>
    <servlet-name>ServletConfigDemo1</servlet-name>
    <servlet-class>gacl.servlet.study.ServletConfigDemo1</servlet-class>
    <!--配置ServletConfigDemo1的初始化参数 -->
    <init-param>
        <param-name>name</param-name>
        <param-value>gacl</param-value>
    </init-param>
     <init-param>
        <param-name>password</param-name>
        <param-value>123</param-value>
    </init-param>
    <init-param>
        <param-name>charset</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</servlet>

3.2、Servlet获取初始化参数

​ 当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,我们通过ServletConfig对象就可以得到当前servlet的初始化参数信息。

public class ServletConfigDemo1 extends HttpServlet {
    private ServletConfig config;
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取在web.xml中配置的初始化参数
        String paramVal = this.config.getInitParameter("name");//获取指定的初始化参数
        response.getWriter().print(paramVal);
        //获取所有的初始化参数
        Enumeration<String> e = config.getInitParameterNames();
        while(e.hasMoreElements()){
            String name = e.nextElement();
            String value = config.getInitParameter(name);
            response.getWriter().print(name + "=" + value + "<br/>");
        }
    }
}

四、ServletContext

4.1、定义

​ WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用。ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,可以通过ServletConfig.getServletContext方法获得ServletContext对象。由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。ServletContext对象通常也被称之为context域对象。

4.2、Servlet间数据共享

public class ServletContextDemo1 extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String data = "xdp_gacl";
        /**
         * ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,
         * 可以通过ServletConfig.getServletContext方法获得ServletContext对象。
         */
        ServletContext context = this.getServletConfig().getServletContext();//获得ServletContext对象
        context.setAttribute("data", data);  //将data存储到ServletContext对象中
    }
}
public class ServletContextDemo2 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext context = this.getServletContext();
        String data = (String) context.getAttribute("data");//从ServletContext对象中取出数据
        response.getWriter().print("data="+data);
    }
}

4.3、获取WEB应用的初始化参数

(1)、使用标签配置WEB应用的初始化参数

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 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">
    <display-name></display-name>
    <!-- 配置WEB应用的初始化参数 -->
    <context-param>
        <param-name>url</param-name>
        <param-value>jdbc:mysql://localhost:3306/test</param-value>
    </context-param>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

(2)、获取Web应用的初始化参数

public class ServletContextDemo3 extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext context = this.getServletContext();
        //获取整个web站点的初始化参数
        String contextInitParam = context.getInitParameter("url");
        response.getWriter().print(contextInitParam);
    }
}

五、Servlet3.0

5.1、新注解

@WebServlet

​ @WebServlet用于将一个类声明为Servlet,该标注将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为Servlet。

@WebServlet(name = "servlet3annotation", urlPatterns = {"/servlet3"},  asyncSupported = true, loadOnStartup = -1)
public class Servlet3Annotation extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取ServletConfig的实例
        ServletConfig config = this.getServletConfig();
        //获取指定参数名称的值
        String name = config.getInitParameter("username");
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head><title>Servlet3应用实例</title></head>");
        out.println("<body>");
        out.print("获取InitParamServlet的初始化参数\"username\"的字符串值:" + name);
        out.println("</body>");
        out.println("</html>");
    }
}

@WebFilter

​ @WebFilter用于将一个类声明为过滤器,该标注将会在部署时被容器处理。以下属性均为可选属性,但是value、urlPatterns、servletNames三者必需至少包含一个,且value和urlPattern不能共存,如果同时指定,通常忽略value的取值。

属性名 类型 描述
filterName String 指定过滤器的name属性,等价于
value String[] 该属性等价于urlPatterns属性,两个属性不能同时使用
urlPatterns String[] 指定一组Servlet的URL匹配模式,等价于标签
servletNames String[] @WebServlet中的name属性的取值,或者是web.xml中的取值
initParams WebInitParam[] 指定一组Servlet初始化参数,等价于标签
dispatcherTypes DispatcherType 指定过滤器的转发模式。具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST
asyncSupported boolean 声明过滤器是否支持异步操作模式,等价于标签
description String 该Servlet的描述信息,等价于标签
displayName String 该Servlet的显示名,通常配合工具使用,等价于标签
@WebFilter(servletNames = {"servlet3filterannotation"}, filterName = "characterFilter",
        initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class Servlet3FilterAnnotation implements Filter {
    private FilterConfig filterConfig = null;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //获取此Filter的初始参数的值
        String encoding = filterConfig.getInitParameter("encoding");
        System.out.println(encoding);
        //设置请求数据的编码方式
        servletRequest.setCharacterEncoding(encoding);
        //把请求和响应对象传给过滤链中的下一个要调用的过滤器或Servlet
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

@WebListener

该标注用于将类声明为监听器。

属性名 类型 是否可选 描述
value String 该监听器的描述信息
@WebListener("This is the Listener")
public class Servlet3Listener implements ServletContextListener {
    @Override
    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
    }
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
    //应用上下文初始化会回调的方法
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //初始化一个application对象
        this.application = servletContextEvent.getServletContext();
        //设置一个列表属性,用于保存在线用户名
        this.application.setAttribute("online",new LinkedList<String>());
    }
}

@WebInitParam

​ @WebInitParam标注通常不单独使用,而是配合@WebServlet或者@WebFilter使用。它的作业是为Servlet或者过滤器指定初始化参数,这等价于web.xml中子标签

属性名 类型 是否可选 描述
name String 指定参数的名字,等价于
value String 指定参数的值,等价于
description String 指定参数的描述,等价于

@MultipartConfig

​ 该标注主要是为了辅助Servlet3.0中HttpServletRequest提供的对上传文件的支持。该标注标注在Servlet上,表示该Servlet希望处理的请求的MIME类型是multipart/form-data。

属性名 类型 是否可选 描述
fileSizeThreshold int 当数据量大于该值时,内容将被写入文件
location String 存放生成的文件地址
maxFileSize long 允许上传的文件最大值。默认值为-1,表示没有限制
maxRequestSize long 针对该multipart/form-data请求的最大数量,默认值为-1,表示没有限制

5.2、异步支持

​ Servlet3.0支持异步处理支持,Servlet接收到请求之后,可能首先需要对请求携带的数据进行一些预处理;接着,Servlet线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,此时Servlet还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有ServletRequest和ServletResponse对象的引用),或者将请求继续转发给其他Servlet。

@WebServlet(urlPatterns = {"/asyncdemo"}, asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
	 @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("进入Servlet的时间:" + new Date() + ".");
        out.flush();
        //在子线程中执行业务调用,并由其负责输出响应,主线程退出
        AsyncContext ctx = req.startAsync();
        new Thread(new Executor(ctx)).start();
        out.println("结束Servlet的时间:" + new Date() + ".");
        out.flush();
    }
    public class Executor implements Runnable {
        private AsyncContext ctx = null;
        public Executor(AsyncContext ctx) {
            this.ctx = ctx;
        }
        public void run() {
            try {
                //等待10秒钟,以模拟业务方法的执行
                Thread.sleep(10000);
                PrintWriter out = ctx.getResponse().getWriter();
                out.println("业务处理完毕的时间:" + new Date() + ".");
                out.flush();
                ctx.complete();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

5.3、ServletContainerInitializer

5.3.1、介绍

​ 在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。
​ 要使用ServletContainerInitializer就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体ServletContainerInitializer实现类,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。
​ Tomcat容器的ServletContainerInitializer机制的实现,主要交由Context容器和ContextConfig监听器共同实现。ContextConfig监听器负责在容器启动时读取每个web应用的WEB-INF/lib目录下包含的jar包的META-INF/services/javax.servlet.ServletContainerInitializer,以及web根目录下的META-INF/services/javax.servlet.ServletContainerInitializer,通过反射完成这些ServletContainerInitializer的实例化,然后再设置到Context容器中。

5.3.2、实现机制

​ Context容器启动时就会分别调用每个ServletContainerInitializer的onStartup方法,并将感兴趣的类作为参数传入。
步骤:

  • 首先通过ContextConfig监听器遍历每个jar包或web根目录的META-INF/services/javax.servlet.ServletContainerInitializer文件,根据读到的类路径实例化每个ServletContainerInitializer。
  • 然后再分别将实例化好的ServletContainerInitializer设置进Context容器中。
  • 最后Context容器启动时分别调用所有ServletContainerInitializer对象的onStartup方法。

5.3.3、@HandlesTypes

​ 容器启动的时候通过@HandlesTypes可以感兴趣类型的所有子类型到ServletContainerInitializerde的onStartup方法作为参数传入。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("rawtypes") // Spec API does not use generics
public @interface HandlesTypes {
    /**
     * @return array of classes
     */
    Class[] value();
}

5.3.4、使用案例

(1)、实现接口重写方法

@HandlesTypes(value={HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
    /**
     * Set<Class<?>> arg0:感兴趣的类型的所有子类型;
     * ServletContext arg1:代表当前Web应用的ServletContext;一个Web应用一个ServletContext;
     */
    @Override
    public void onStartup(Set<Class<?>> arg0, ServletContext sc) throws ServletException {
        for (Class<?> claz : arg0) {
            System.out.println(claz);
        }
        
        //注册组件  ServletRegistration  
        ServletRegistration.Dynamic servlet = sc.addServlet("userServlet", new UserServlet());
        //配置servlet的映射信息
        servlet.addMapping("/user");        
     
        //注册Listener
        sc.addListener(UserListener.class);
        
        //注册Filter  FilterRegistration
        FilterRegistration.Dynamic filter = sc.addFilter("userFilter", UserFilter.class);
        //配置Filter的映射信息
        filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); 
    }
}

(2)、编写文件绑定实现类

​ 在META-INF/services目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类。

posted @ 2020-06-01 23:23  ciyelc  阅读(93)  评论(0)    收藏  举报