Spring Boot學習5:spring-boot web容器

一枚程式設計師發表於2018-02-11
1傳統Servlet容器
1.1Eclipse Jetty:是一個嵌入式的容器,最新版本jetty9.0。支援的功能如下:
    非同步http server
    標準的servlet容器
    websocket
    http/2 server
    asynchronous Client(http/1.1, http/2, websocket)  Java7開始才有AIO
    OSGI,JNDI,JMX,JASPI,AJP support


1.2 Apache Tomcat:
1.2.1 標準實現:
    Servlet
    JSP
    Expression Language
    WebSocket

1.2.2 Apache Tomcat
1)核心元件Components
  Engine
    
 

  Host
  管理主機

  Context:是tomcat運維中重要的一塊
  和Application同等級別,類似於ServletContext

  我們可以檢視Tomcat的配置檔案server.xml
  注意:Engin中有Host,Host中有Context,Tomcat8.5中Host並沒有配置Context,後面的版本建議在Host中配置Context(見Context.xml)

2)靜態資源處理
  檢視web.xml配置檔案servlet節點
    <servlet>
        <servlet-name>default</servlet-name> <!--定義了defualt的servlet,可以到配置檔案中查詢對應的對映Servlet-->
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>  <!--是否開啟debug-->
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name> <!--是否展示選單列表-->
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

 
  例子:在Idea中建立專案web application

  啟動專案,訪問:http://localhost:8080/tomcat-test/
  頁面預設返回index.jsp頁面

  在webapp中建立index.html靜態檔案,重啟訪問:http://localhost:8080/tomcat-test/index.html
  頁面返回index.html內容,其實任何一個請求都會走ServletContext

3)歡迎頁面
  檢視web.xml配置檔案
      <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
      </welcome-file-list>
4)JSP處理

 
5)類載入
例子:
package com.segmentfault.lesson6;

public class Demo {
    public static void main(String[] args) {

        //檢視載入本類的所有的ClassLoader
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(classLoader.getClass().getName());
        while(true){
            classLoader = classLoader.getParent();
            if(classLoader!=null){
                System.out.println(classLoader.getClass().getName());
            }
            else{
                break;
            }
        }

        //獲取當前程式的SystemClassLoader
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader.getClass().getName());


    }
}

列印結果:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader



例子:建立一個Listener的Servlet類
檢視ServletContext的classloader過程
package com.segmentfault.lesson6;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ServletContextListenerImpl implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext sc = servletContextEvent.getServletContext();
        ClassLoader classLoader = sc.getClassLoader();
        System.out.println(classLoader.getClass().getName());
        while(true){
            classLoader = classLoader.getParent();
            if(classLoader!=null){
                System.out.println(classLoader.getClass().getName());
            }else{
                break;
            }
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}


控制檯列印結果:
org.apache.catalina.loader.ParallelWebappClassLoader
java.net.URLClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

6)聯結器
在tomcat的server.xml配置中:
    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
    -->
    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>

埠(port)



協議(Protocol)

連線池(Thread Pool)

超時時間(Timeout)


等等。。。



我們可以檢視Connector類的原始碼(匯入tomcat的原始碼)
可以看到裡面的port,
   /**
     * Coyote Protocol handler class name.
     * Defaults to the Coyote HTTP/1.1 protocolHandler.
     */
    protected String protocolHandlerClassName =
        "org.apache.coyote.http11.Http11NioProtocol";



    /**
     * @return the Coyote protocol handler in use.
     */
    public String getProtocol() {
        if (("org.apache.coyote.http11.Http11NioProtocol".equals(getProtocolHandlerClassName()) &&
                    (!AprLifecycleListener.isAprAvailable() || !AprLifecycleListener.getUseAprConnector())) ||
                "org.apache.coyote.http11.Http11AprProtocol".equals(getProtocolHandlerClassName()) &&
                    AprLifecycleListener.getUseAprConnector()) {
            return "HTTP/1.1";
        } else if (("org.apache.coyote.ajp.AjpNioProtocol".equals(getProtocolHandlerClassName()) &&
                    (!AprLifecycleListener.isAprAvailable() || !AprLifecycleListener.getUseAprConnector())) ||
                "org.apache.coyote.ajp.AjpAprProtocol".equals(getProtocolHandlerClassName()) &&
                    AprLifecycleListener.getUseAprConnector()) {
            return "AJP/1.3";
        }
        return getProtocolHandlerClassName();
    }

從這裡也可以看出,我們的很多屬性都是可以設定的,執行的時候顯示的資訊: Starting ProtocolHandler ["http-nio-8888"],其中的nio就是協議,所以任何的顯示都是有根據的。


 
7)JDBC資料來源
8)JNDI(Java Naming and Directory Interface

    <Resource name="jdbc/UserDatabase"   目錄的名稱
          auth="Container"   認證方式:容器內部
              type="org.apache.catalina.UserDatabase"  介面名稱
          <!--type="javax.sql.DataSource"  -->
          maxTotal="100" 最大連線數
          maxIdle="30"  所謂的Idle就是不活動連線數
          maxWaitMillis="10000"   最大等待時間10000毫秒
          username="javauser" password="javadude"   賬號密碼
          driverClassName="com.mysql.jdbc.Driver"    驅動名稱
          url="jdbc:mysql://localhost:3306/javatest"   資料庫連線
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />



使用Eclipse工具
例子:配置tomcat中的Context.xml檔案,配置資料庫資訊
7.1)首先建立資料庫testdemo,並建立user表
create database testdemo;
mysql> create table user(id int primary key auto_increment,
    -> name varchar(100));

7.2)修改Server中的Context.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
--><!-- The contents of this file will be loaded for each web application --><Context>

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
    <Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
        username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/"
    />
</Context>

7.3)配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <listener>
        <listener-class>com.segmentfault.lesson6.ServletContextListenerImpl</listener-class>
    </listener>
    
    <resource-ref>
        <res-ref-name>jdbc/testdemo</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
      
    <env-entry>
    <env-entry-name>Bean</env-entry-name>
    <env-entry-type>java.lang.String</env-entry-type>
    <env-entry-value>hello</env-entry-value>
    </env-entry>
</web-app>

7.4)編寫java測試程式碼
package com.spring.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;


public class JdbcServlet extends HttpServlet{
    private DataSource dataSource;
    @Override
    public void init() throws ServletException {
        try {
            Context context = new InitialContext();
            Context envContext = (Context) context.lookup("java:comp/env");
            dataSource = (DataSource) envContext.lookup("jdbc/TestDB");
            String bean = (String) envContext.lookup("Bean");
            System.out.println(bean);
        } catch (NamingException e) {
            e.printStackTrace();
        }
        
    }
    
    @Override
    protected void service(HttpServletRequest arg0, HttpServletResponse resp) throws ServletException, IOException {
        Writer writer = resp.getWriter();
        try {
            Connection connection = dataSource.getConnection();
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("show databases;");
            while(resultSet.next()) {
                writer.write(resultSet.getString(1));
                System.out.println(resultSet.getString(1));
            }
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}


執行後:
控制檯列印:
hello
information_schema
mysql
performance_schema
test
testdemo

這個其實就是依賴反轉的原理和鼻祖

執行:報錯
Caused by: java.sql.SQLException: No suitable driver
顯然,沒有驅動,將驅動包複製到專案中,並引用進來

另外,如果出現許可權安全問題
在url後加上?useSSL=false



 

 




2 Spring Boot嵌入式Web容器

1)使用IDEA軟體
開啟applicationContext.xml配置server.port,並查詢原始碼的地方
org.springframework.boot.autoconfigure.web.ServerProperties
對應的配置
    {
      "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties",
      "name": "server.port",
      "description": "Server HTTP port.",
      "type": "java.lang.Integer"
    },


然後我們可以找到getPort的地方:
這個customize方法中,設定了port,address,contextPath, SSL,timeout等等
@Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        if (getPort() != null) {
            container.setPort(getPort());
        }
        if (getAddress() != null) {
            container.setAddress(getAddress());
        }
        if (getContextPath() != null) {
            container.setContextPath(getContextPath());
        }
        if (getDisplayName() != null) {
            container.setDisplayName(getDisplayName());
        }
        if (getSession().getTimeout() != null) {
            container.setSessionTimeout(getSession().getTimeout());
        }
        container.setPersistSession(getSession().isPersistent());
        container.setSessionStoreDir(getSession().getStoreDir());
        if (getSsl() != null) {
            container.setSsl(getSsl());
        }
        if (getJspServlet() != null) {
            container.setJspServlet(getJspServlet());
        }
        if (getCompression() != null) {
            container.setCompression(getCompression());
        }
        container.setServerHeader(getServerHeader());
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            getTomcat().customizeTomcat(this,
                    (TomcatEmbeddedServletContainerFactory) container);
        }
        if (container instanceof JettyEmbeddedServletContainerFactory) {
            getJetty().customizeJetty(this,
                    (JettyEmbeddedServletContainerFactory) container);
        }

        if (container instanceof UndertowEmbeddedServletContainerFactory) {
            getUndertow().customizeUndertow(this,
                    (UndertowEmbeddedServletContainerFactory) container);
        }
        container.addInitializers(new SessionConfiguringInitializer(this.session));
        container.addInitializers(new InitParameterConfiguringServletContextInitializer(
                getContextParameters()));
    }


其中,方法customize是介面EmbeddedServletContainerCustomizer的方法,這個是嵌入式servlet容器引擎
public interface EmbeddedServletContainerCustomizer {

    /**
     * Customize the specified {@link ConfigurableEmbeddedServletContainer}.
     * @param container the container to customize
     */
    void customize(ConfigurableEmbeddedServletContainer container);

}


我們來看一下嵌入式引擎的connector
查詢到Connector,檢視原始碼中的setProtocol程式碼
    @Deprecated
    public void setProtocol(String protocol) {

        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector();

        if ("HTTP/1.1".equals(protocol) || protocol == null) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
            }
        } else if ("AJP/1.3".equals(protocol)) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
            }
        } else {
            setProtocolHandlerClassName(protocol);
        }
    }


可以看到,這個setProtocol已經是過時的方法了,建議使用構造器的方法進行初始化


    public Connector(String protocol) {
        setProtocol(protocol);
        // Instantiate protocol handler
        ProtocolHandler p = null;
        try {
            Class<?> clazz = Class.forName(protocolHandlerClassName);
            p = (ProtocolHandler) clazz.getConstructor().newInstance();
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        } finally {
            this.protocolHandler = p;
        }

        if (Globals.STRICT_SERVLET_COMPLIANCE) {
            uriCharset = StandardCharsets.ISO_8859_1;
        } else {
            uriCharset = StandardCharsets.UTF_8;
        }
    }


預設的Protocol是
    /**
     * The class name of default protocol used.
     */
    public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";


那麼,我們可以在哪裡修改這個protocol呢?

重寫:EmbeddedServletContainerCustomizer的customize方法

注入:
    private List<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<TomcatConnectorCustomizer>();
並將connector設定對應的port和protocol,並加入到tomcat嵌入式servlet工廠中

程式碼實現:
package com.segmentfault.springbootlesson6;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11Nio2Protocol;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class SpringBootLesson6Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootLesson6Application.class, args);
    }

    @GetMapping("/hello")
    public String index(){
        return "hello,world";
    }

    @Bean
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer(){
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if(container instanceof TomcatEmbeddedServletContainerFactory){
                    TomcatEmbeddedServletContainerFactory factory = TomcatEmbeddedServletContainerFactory.class.cast(container);
                    factory.addContextCustomizers(new TomcatContextCustomizer() {
                        @Override
                        public void customize(Context context) {
                            context.setPath("/spring-boot");
                        }
                    });

                    factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
                        @Override
                        public void customize(Connector connector) {
                            connector.setPort(8888);
                            //這個方法已經過時的
                            connector.setProtocol(Http11Nio2Protocol.class.getName());
                        }
                    });
                }
            }
        };
    }
}



其中測試:可以看到控制檯中列印

2018-02-12 10:05:44.971  INFO 17768 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http)

然後開啟jconsole,檢視該java程式中的http協議





同樣的,我們也可以手寫修改context

factory.addContextCustomizers(new TomcatContextCustomizer() {
    @Override
    public void customize(Context context) {
        context.setPath("/spring-boot");
    }
});

並建立一個介面:

    @GetMapping("/hello")
    public String index(){
        return "hello,world";
    }

訪問:http://localhost:8888/spring-boot/hello

返回hello,world





3 Q&A

1)jndi在實際開發中有什麼用?
jndi告訴我們,不要直接使用new的方式生成物件,而是用get的方法從容器中取。
如果設定了密碼,我們可以使用jndi的方式,到指定的路徑取密碼,而不用直接配置密碼

jndi對應spring的上下文,和classloader不同。是以虛擬路徑的方式如 jdbc/testDB等
資源以樹形結構的方式儲存資源



2)微服務和j2ee
微服務強調的是無狀態

j2ee強調有狀態


3)嵌入式的tomcat是怎麼搭配叢集的?
因為強調的是無狀態,所以叢集容易搭建,類似於克隆
無狀態,每個機器上不儲存使用者資訊

相關文章