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

Servlet

標籤 : Java與Web


Upload-上傳

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


客戶端

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

<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接口:

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

@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:

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.

文件流解析

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

------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個目錄,且分佈較爲均勻.


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

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

  • 客戶端
<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目錄下.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章