Servlet - Upload、Download、Async、動態註冊

Servlet

標籤 : Java與Webjavascript


Upload-上傳

隨着3.0版本的發佈,文件上傳終於成爲Servlet規範的一項內置特性,再也不依賴於像Commons FileUpload之類組件,所以在服務端進行文件上傳編程變得不費吹灰之力.html


客戶端

要上傳文件, 必須利用multipart/form-data設置HTML表單的enctype屬性,且method必須爲POST:java

<form action="simple_file_upload_servlet.do" method="POST" enctype="multipart/form-data">
    <table align="center" border="1" width="50%">
        <tr>
            <td>Author:</td>
            <td><input type="text" name="author"></td>
        </tr>
        <tr>
            <td>Select file to Upload:</td>
            <td><input type="file" name="file"></td>
        </tr>
        <tr>
            <td><input type="submit" value="上傳"></td>
        </tr>
    </table>
</form>

服務端

服務端Servlet主要圍繞着@MultipartConfig註解和Part接口:web

處理上傳文件的Servlet必須用@MultipartConfig註解標註:spring

@MultipartConfig屬性 描述
fileSizeThreshold The size threshold after which the file will be written to disk
location The directory location where files will be stored
maxFileSize The maximum size allowed for uploaded files.
maxRequestSize The maximum size allowed for multipart/form-data requests

在一個由多部件組成的請求中, 每個表單域(包括非文件域), 都會被封裝成一個Part,HttpServletRequest中提供以下兩個方法獲取封裝好的Part:apache

HttpServletRequest 描述
Part getPart(String name) Gets the Part with the given name.
Collection<Part> getParts() Gets all the Part components of this request, provided that it is of type multipart/form-data.

Part中提供了以下經常使用方法來獲取/操做上傳的文件/數據:編程

Part 描述
InputStream getInputStream() Gets the content of this part as an InputStream
void write(String fileName) A convenience method to write this uploaded item to disk.
String getSubmittedFileName() Gets the file name specified by the client(須要有Tomcat 8.x 及以上版本支持)
long getSize() Returns the size of this fille.
void delete() Deletes the underlying storage for a file item, including deleting any associated temporary disk file.
String getName() Gets the name of this part
String getContentType() Gets the content type of this part.
Collection<String> getHeaderNames() Gets the header names of this Part.
String getHeader(String name) Returns the value of the specified mime header as a String.

文件流解析

經過抓包獲取到客戶端上傳文件的數據格式:api

------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="author"

feiqing
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="file"; filename="memcached.txt"
Content-Type: text/plain


------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh--
能夠看到:
A. 若是HTML表單輸入項爲文本( <input type="text"/>),將只包含一個請求頭 Content-Disposition.
B. 若是HTML表單輸入項爲文件( <input type="file"/>), 則包含兩個頭:
Content-DispositionContent-Type.

在Servlet中處理上傳文件時, 須要:瀏覽器

- 經過查看是否存在`Content-Type`標頭, 檢驗一個Part是封裝的普通表單域,仍是文件域.
- 如有`Content-Type`存在, 但文件名爲空, 則表示沒有選擇要上傳的文件.
- 若是有文件存在, 則能夠調用`write()`方法來寫入磁盤, 調用同時傳遞一個絕對路徑, 或是相對於`@MultipartConfig`註解的`location`屬性的相對路徑.
  • SimpleFileUploadServlet
/** * @author jifang. * @since 2016/5/8 16:27. */
@MultipartConfig
@WebServlet(name = "SimpleFileUploadServlet", urlPatterns = "/simple_file_upload_servlet.do")
public class SimpleFileUploadServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        Part file = request.getPart("file");
        if (!isFileValid(file)) {
            writer.print("<h1>請確認上傳文件是否正確!");
        } else {
            String fileName = file.getSubmittedFileName();
            String saveDir = getServletContext().getRealPath("/WEB-INF/files/");
            mkdirs(saveDir);
            file.write(saveDir + fileName);

            writer.print("<h3>Uploaded file name: " + fileName);
            writer.print("<h3>Size: " + file.getSize());
            writer.print("<h3>Author: " + request.getParameter("author"));
        }
    }

    private void mkdirs(String saveDir) {
        File dir = new File(saveDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
    }

    private boolean isFileValid(Part file) {
        // 上傳的並不是文件
        if (file.getContentType() == null) {
            return false;
        }
        // 沒有選擇任何文件
        else if (Strings.isNullOrEmpty(file.getSubmittedFileName())) {
            return false;
        }
        return true;
    }
}

優化

  • 善用WEB-INF
    存放在/WEB-INF/目錄下的資源沒法在瀏覽器地址欄直接訪問, 利用這一特色可將某些受保護資源存放在WEB-INF目錄下, 禁止用戶直接訪問(如用戶上傳的可執行文件,如JSP等),以防被惡意執行, 形成服務器信息泄露等危險.
getServletContext().getRealPath("/WEB-INF/")
  • 文件名亂碼
    當文件名包含中文時,可能會出現亂碼,其解決方案與POST相同:
request.setCharacterEncoding("UTF-8");
  • 避免文件同名
    若是上傳同名文件,會形成文件覆蓋.所以能夠爲每份文件生成一個惟一ID,而後鏈接原始文件名:
private String generateUUID() {
    return UUID.randomUUID().toString().replace("-", "_");
}
  • 目錄打散
    若是一個目錄下存放的文件過多, 會致使文件檢索速度降低,所以須要將文件打散存放到不一樣目錄中, 在此咱們採用Hash打散法(根據文件名生成Hash值, 取Hash值的前兩個字符做爲二級目錄名), 將文件分佈到一個二級目錄中:
private String generateTwoLevelDir(String destFileName) {
    String hash = Integer.toHexString(destFileName.hashCode());
    return String.format("%s/%s", hash.charAt(0), hash.charAt(1));
}

採用Hash打散的好處是:在根目錄下最多生成16個目錄,而每一個子目錄下最多再生成16個子子目錄,即一共256個目錄,且分佈較爲均勻.ruby


示例-簡易存儲圖片服務器

需求: 提供上傳圖片功能, 爲其生成外鏈, 並提供下載功能(見下)

  • 客戶端
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>IFS</title>
</head>
<body>
<form action="ifs_upload.action" method="POST" enctype="multipart/form-data">
    <table align="center" border="1" width="50%">
        <tr>
            <td>Select A Image to Upload:</td>
            <td><input type="file" name="image"></td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><input type="submit" value="上傳"></td>
        </tr>
    </table>
</form>
</body>
</html>
  • 服務端
@MultipartConfig
@WebServlet(name = "ImageFileUploadServlet", urlPatterns = "/ifs_upload.action")
public class ImageFileUploadServlet extends HttpServlet {

    private Set<String> imageSuffix = new HashSet<>();

    private static final String SAVE_ROOT_DIR = "/images";

    {
        imageSuffix.add(".jpg");
        imageSuffix.add(".png");
        imageSuffix.add(".jpeg");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        Part image = request.getPart("image");
        String fileName = getFileName(image);
        if (isFileValid(image, fileName) && isImageValid(fileName)) {
            String destFileName = generateDestFileName(fileName);
            String twoLevelDir = generateTwoLevelDir(destFileName);

            // 保存文件
            String saveDir = String.format("%s/%s/", getServletContext().getRealPath(SAVE_ROOT_DIR), twoLevelDir);
            makeDirs(saveDir);
            image.write(saveDir + destFileName);

            // 生成外鏈
            String ip = request.getLocalAddr();
            int port = request.getLocalPort();
            String path = request.getContextPath();
            String urlPrefix = String.format("http://%s:%s%s", ip, port, path);
            String urlSuffix = String.format("%s/%s/%s", SAVE_ROOT_DIR, twoLevelDir, destFileName);
            String url = urlPrefix + urlSuffix;
            String result = String.format("<a href=%s>%s</a><hr/><a href=ifs_download.action?location=%s>下載</a>",
                    url,
                    url,
                    saveDir + destFileName);
            writer.print(result);
        } else {
            writer.print("Error : Image Type Error");
        }
    }

    /** * 校驗文件表單域有效 * * @param file * @param fileName * @return */
    private boolean isFileValid(Part file, String fileName) {
        // 上傳的並不是文件
        if (file.getContentType() == null) {
            return false;
        }
        // 沒有選擇任何文件
        else if (Strings.isNullOrEmpty(fileName)) {
            return false;
        }

        return true;
    }

    /** * 校驗文件後綴有效 * * @param fileName * @return */
    private boolean isImageValid(String fileName) {
        for (String suffix : imageSuffix) {
            if (fileName.endsWith(suffix)) {
                return true;
            }
        }
        return false;
    }

    /** * 加速圖片訪問速度, 生成兩級存放目錄 * * @param destFileName * @return */
    private String generateTwoLevelDir(String destFileName) {
        String hash = Integer.toHexString(destFileName.hashCode());
        return String.format("%s/%s", hash.charAt(0), hash.charAt(1));
    }

    private String generateUUID() {
        return UUID.randomUUID().toString().replace("-", "_");
    }

    private String generateDestFileName(String fileName) {
        String destFileName = generateUUID();
        int index = fileName.lastIndexOf(".");
        if (index != -1) {
            destFileName += fileName.substring(index);
        }
        return destFileName;
    }

    private String getFileName(Part part) {
        String[] elements = part.getHeader("content-disposition").split(";");
        for (String element : elements) {
            if (element.trim().startsWith("filename")) {
                return element.substring(element.indexOf("=") + 1).trim().replace("\"", "");
            }
        }
        return null;
    }

    private void makeDirs(String saveDir) {
        File dir = new File(saveDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
    }
}

因爲getSubmittedFileName()方法須要有Tomcat 8.X以上版本的支持, 所以爲了通用期間, 咱們本身解析content-disposition請求頭, 獲取filename.


Download-下載

文件下載是向客戶端響應二進制數據(而非字符),瀏覽器不會直接顯示這些內容,而是會彈出一個下載框, 提示下載信息.

爲了將資源發送給瀏覽器, 須要在Servlet中完成如下工做:

  • 使用Content-Type響應頭來規定響應體的MIME類型, 如image/pjpegapplication/octet-stream;
  • 添加Content-Disposition響應頭,賦值爲attachment;filename=xxx.yyy, 設置文件名;
  • 使用response.getOutputStream()給瀏覽器發送二進制數據;

文件名中文亂碼

當文件名包含中文時(attachment;filename=文件名.後綴名),在下載框中會出現亂碼, 須要對文件名編碼後在發送, 但不一樣的瀏覽器接收的編碼方式不一樣:

* FireFox: Base64編碼
    * 其餘大部分Browser: URL編碼

所以最好將其封裝成一個通用方法:

private String filenameEncoding(String filename, HttpServletRequest request) throws IOException {
    // 根據瀏覽器信息判斷
    if (request.getHeader("User-Agent").contains("Firefox")) {
        filename = String.format("=?utf-8?B?%s?=", BaseEncoding.base64().encode(filename.getBytes("UTF-8")));
    } else {
        filename = URLEncoder.encode(filename, "utf-8");
    }
    return filename;
}

示例-IFS下載功能

/** * @author jifang. * @since 2016/5/9 17:50. */
@WebServlet(name = "ImageFileDownloadServlet", urlPatterns = "/ifs_download.action")
public class ImageFileDownloadServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("application/octet-stream");
        String fileLocation = request.getParameter("location");
        String fileName = fileLocation.substring(fileLocation.lastIndexOf("/") + 1);
        response.setHeader("Content-Disposition", "attachment;filename=" + filenameEncoding(fileName, request));

        ByteStreams.copy(new FileInputStream(fileLocation), response.getOutputStream());
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

Async-異步處理

Servlet/Filter默認會一直佔用請求處理線程, 直到它完成任務.若是任務耗時長久, 且併發用戶請求量大, Servlet容器將會遇到超出線程數的風險.

Servlet 3.0 中新增了一項特性, 用來處理異步操做. 當Servlet/Filter應用程序中有一個/多個長時間運行的任務時, 你能夠選擇將任務分配給一個新的線程, 從而將當前請求處理線程返回到線程池中,釋放線程資源,準備爲下一個請求服務.


異步Servlet/Filter

  • 異步支持
    @WebServlet/@WebFilter註解提供了新的asyncSupport屬性:
@WebFilter(asyncSupported = true)
@WebServlet(asyncSupported = true)

一樣部署描述符中也添加了<async-supportted/>標籤:

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.fq.web.servlet.HelloServlet</servlet-class>
    <async-supported>true</async-supported>
</servlet>
  • Servlet/Filter
    支持異步處理的Servlet/Filter能夠經過在ServletRequest中調用startAsync()方法來啓動新線程:
ServletRequest 描述
AsyncContext startAsync() Puts this request into asynchronous mode, and initializes its AsyncContext with the original (unwrapped) ServletRequest and ServletResponse objects.
AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) Puts this request into asynchronous mode, and initializes its AsyncContext with the given request and response objects.

注意:
1. 只能將原始的ServletRequest/ServletResponse或其包裝器(Wrapper/Decorator,詳見Servlet - Listener、Filter、Decorator)傳遞給第二個startAsync()方法.
2. 重複調用startAsync()方法會返回相同的AsyncContext實例, 若是在不支持異步處理的Servlet/Filter中調用, 會拋出java.lang.IllegalStateException異常.
3. AsyncContextstart()方法不會形成方法阻塞.

這兩個方法都返回AsyncContext實例, AsyncContext中提供了以下經常使用方法:

AsyncContext 描述
void start(Runnable run) Causes the container to dispatch a thread, possibly from a managed thread pool, to run the specified Runnable.
void dispatch(String path) Dispatches the request and response objects of this AsyncContext to the given path.
void dispatch(ServletContext context, String path) Dispatches the request and response objects of this AsyncContext to the given path scoped to the given context.
void addListener(AsyncListener listener) Registers the given AsyncListener with the most recent asynchronous cycle that was started by a call to one of the ServletRequest.startAsync() methods.
ServletRequest getRequest() Gets the request that was used to initialize this AsyncContext by calling ServletRequest.startAsync() or ServletRequest.startAsync(ServletRequest, ServletResponse).
ServletResponse getResponse() Gets the response that was used to initialize this AsyncContext by calling ServletRequest.startAsync() or ServletRequest.startAsync(ServletRequest, ServletResponse).
boolean hasOriginalRequestAndResponse() Checks if this AsyncContext was initialized with the original or application-wrapped request and response objects.
void setTimeout(long timeout) Sets the timeout (in milliseconds) for this AsyncContext.

在異步Servlet/Filter中須要完成如下工做, 才能真正達到異步的目的:

  • 調用AsyncContextstart()方法, 傳遞一個執行長時間任務的Runnable;
  • 任務完成時, 在Runnable內調用AsyncContextcomplete()方法或dispatch()方法

示例-改造文件上傳

在前面的圖片存儲服務器中, 若是上傳圖片過大, 可能會耗時長久,爲了提高服務器性能, 可將其改造爲異步上傳(其改形成本較小):

@Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
    final AsyncContext asyncContext = request.startAsync();
    asyncContext.start(new Runnable() {
        @Override
        public void run() {
            try {
                request.setCharacterEncoding("UTF-8");
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter writer = response.getWriter();
                Part image = request.getPart("image");
                final String fileName = getFileName(image);
                if (isFileValid(image, fileName) && isImageValid(fileName)) {
                    String destFileName = generateDestFileName(fileName);
                    String twoLevelDir = generateTwoLevelDir(destFileName);

                    // 保存文件
                    String saveDir = String.format("%s/%s/", getServletContext().getRealPath(SAVE_ROOT_DIR), twoLevelDir);
                    makeDirs(saveDir);
                    image.write(saveDir + destFileName);
                    // 生成外鏈
                    String ip = request.getLocalAddr();
                    int port = request.getLocalPort();
                    String path = request.getContextPath();
                    String urlPrefix = String.format("http://%s:%s%s", ip, port, path);
                    String urlSuffix = String.format("%s/%s/%s", SAVE_ROOT_DIR, twoLevelDir, destFileName);
                    String url = urlPrefix + urlSuffix;
                    String result = String.format("<a href=%s>%s</a><hr/><a href=ifs_download.action?location=%s>下載</a>",
                            url,
                            url,
                            saveDir + destFileName);
                    writer.print(result);
                } else {
                    writer.print("Error : Image Type Error");
                }
                asyncContext.complete();
            } catch (ServletException | IOException e) {
                LOGGER.error("error: ", e);
            }
        }
    });
}

注意: Servlet異步支持只適用於長時間運行,且想讓用戶知道執行結果的任務. 若是隻有長時間, 但用戶不須要知道處理結果,那麼只需提供一個Runnable提交給Executor, 並當即返回便可.


AsyncListener

Servlet 3.0 還新增了一個AsyncListener接口, 以便通知用戶在異步處理期間發生的事件, 該接口會在異步操做的啓動/完成/失敗/超時狀況下調用其對應方法:

  • ImageUploadListener
/** * @author jifang. * @since 2016/5/10 17:33. */
public class ImageUploadListener implements AsyncListener {

    @Override
    public void onComplete(AsyncEvent event) throws IOException {
        System.out.println("onComplete...");
    }

    @Override
    public void onTimeout(AsyncEvent event) throws IOException {
        System.out.println("onTimeout...");
    }

    @Override
    public void onError(AsyncEvent event) throws IOException {
        System.out.println("onError...");
    }

    @Override
    public void onStartAsync(AsyncEvent event) throws IOException {
        System.out.println("onStartAsync...");
    }
}

與其餘監聽器不一樣, 他沒有@WebListener標註AsyncListener的實現, 所以必須對有興趣收到通知的每一個AsyncContext都手動註冊一個AsyncListener:

asyncContext.addListener(new ImageUploadListener());

動態註冊

動態註冊是Servlet 3.0新特性,它不須要從新加載應用即可安裝新的Web對象(Servlet/Filter/Listener等).


API支持

爲了使動態註冊成爲可能, ServletContext接口添加了以下方法用於 建立/添加 Web對象:

ServletContext 描述
Create
<T extends Servlet> T createServlet(Class<T> clazz) Instantiates the given Servlet class.
<T extends Filter> T createFilter(Class<T> clazz) Instantiates the given Filter class.
<T extends EventListener> T createListener(Class<T> clazz) Instantiates the given EventListener class.
Add
ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) Registers the given servlet instance with this ServletContext under the given servletName.
FilterRegistration.Dynamic addFilter(String filterName, Filter filter) Registers the given filter instance with this ServletContext under the given filterName.
<T extends EventListener> void addListener(T t) Adds the given listener to this ServletContext.
Create & And
ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) Adds the servlet with the given name and class type to this servlet context.
ServletRegistration.Dynamic addServlet(String servletName, String className) Adds the servlet with the given name and class name to this servlet context.
FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) Adds the filter with the given name and class type to this servlet context.
FilterRegistration.Dynamic addFilter(String filterName, String className) Adds the filter with the given name and class name to this servlet context.
void addListener(Class<? extends EventListener> listenerClass) Adds a listener of the given class type to this ServletContext.
void addListener(String className) Adds the listener with the given class name to this ServletContext.

其中addServlet()/addFilter()方法的返回值是ServletRegistration.Dynamic/FilterRegistration.Dynamic,他們都是Registration.Dynamic的子接口,用於動態配置Servlet/Filter實例.


示例-DynamicServlet

動態註冊DynamicServlet, 注意: 並未使用web.xml@WebServlet靜態註冊DynamicServlet實例, 而是用DynRegListener在服務器啓動時動態註冊.

  • DynamicServlet
/** * @author jifang. * @since 2016/5/13 16:41. */
public class DynamicServlet extends HttpServlet {

    private String dynamicName;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().print("<h1>DynamicServlet, MyDynamicName: " + getDynamicName() + "</h1>");
    }

    public String getDynamicName() {
        return dynamicName;
    }

    public void setDynamicName(String dynamicName) {
        this.dynamicName = dynamicName;
    }
}
  • DynRegListener
@WebListener
public class DynRegListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();

        DynamicServlet servlet;
        try {
            servlet = context.createServlet(DynamicServlet.class);
        } catch (ServletException e) {
            servlet = null;
        }

        if (servlet != null) {
            servlet.setDynamicName("Hello fQ Servlet");
            ServletRegistration.Dynamic dynamic = context.addServlet("dynamic_servlet", servlet);
            dynamic.addMapping("/dynamic_servlet.do");
        }

    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
    }
}

容器初始化

在使用相似SpringMVC這樣的MVC框架時,須要首先註冊DispatcherServletweb.xml以完成URL的轉發映射:

<!-- 配置SpringMVC -->
<servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/mvc-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

在Servlet 3.0中,經過Servlet容器初始化,能夠自動完成Web對象的首次註冊,所以能夠省略這個步驟.


API支持

容器初始化的核心是javax.servlet.ServletContainerInitializer接口,他只包含一個方法:

ServletContainerInitializer 描述
void onStartup(Set<Class<?>> c, ServletContext ctx) Notifies this ServletContainerInitializer of the startup of the application represented by the given ServletContext.

在執行任何ServletContext監聽器以前, 由Servlet容器自動調用onStartup()方法.

注意: 任何實現了ServletContainerInitializer的類必須使用@HandlesTypes註解標註, 以聲明該初始化程序能夠處理這些類型的類.


實例-SpringMVC初始化

利用Servlet容器初始化, SpringMVC可實現容器的零配置註冊.

  • SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer) waiClass.newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        AnnotationAwareOrderComparator.sort(initializers);
        servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);

        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}

SpringMVC爲ServletContainerInitializer提供了實現類SpringServletContainerInitializer經過查看源代碼能夠知道,咱們只需提供WebApplicationInitializer的實現類到classpath下, 便可完成對所需Servlet/Filter/Listener的註冊.

public interface WebApplicationInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

詳細可參考springmvc基於java config的實現

  • javax.servlet.ServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer

元數據文件javax.servlet.ServletContainerInitializer只有一行內容(即實現了ServletContainerInitializer類的全限定名),該文本文件必須放在jar包的META-INF/services目錄下.

相關文章
相關標籤/搜索