解決SpringBoot沒法讀取js/css靜態資源的新方法

前言

做爲依賴使用的SpringBoot工程很容易出現自身靜態資源被主工程忽略的狀況。可是做爲依賴而存在的Controller方法卻不會失效,咱們知道,Spring MVC對於靜態資源的處理也不外乎是路徑匹配,讀取資源封裝到Response中響應給瀏覽器,因此,解決的途徑就是本身寫一個讀取Classpath下靜態文件並響應給客戶端的方法。javascript

對於ClassPath下文件的讀取,最容易出現的就是IDE運行ok,打成jar包就沒法訪問了,該問題的緣由仍是在於getResources()不如getResourceAsStream()方法靠譜。css

讀取classpath文件

本就是SpringBoot的問題場景,何不用Spring現成的ClassPathResource類呢?html

ReadClasspathFile.javajava

public class ReadClasspathFile {
    public static String read(String classPath) throws IOException {
        ClassPathResource resource = new ClassPathResource(classPath);
        BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream(),"UTF-8"));
        StringBuilder builder = new StringBuilder();
        String line;
        while ((line = reader.readLine())!=null){
            builder.append(line+"\n");
        }
        return builder.toString();
    }
}

上面的代碼並非特別規範,存在多處漏洞。好比沒有關閉IO流,沒有判斷文件是否存在,沒有考慮到使用緩存進行優化。數組

這裏爲何考慮緩存呢?若是不加緩存,那麼每次請求都涉及IO操做,開銷也比較大。關於緩存的設計,這裏使用WeakHashMap,最終代碼以下:瀏覽器

public class ReadClasspathFile {
    
    private static WeakHashMap<String, String> map = new WeakHashMap<>();

    public static String read(String classPath) {
        //考慮到數據的一致性,這裏沒有使用map的containsKey()
        String s = map.get(classPath);
        if (s != null) {
            return s;
        }
        //判空
        ClassPathResource resource = new ClassPathResource(classPath);
        if (!resource.exists()) {
            return null;
        }
        //讀取
        StringBuilder builder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //DCL雙檢查鎖
        if (!map.containsKey(classPath)) {
            synchronized (ReadClasspathFile.class) {
                if (!map.containsKey(classPath)) {
                    map.put(classPath, builder.toString());
                }
            }
        }
        return builder.toString();
    }
}

但這樣就完美了嗎?其實否則。對於html/css等文本文件,這樣看起來彷佛並無什麼錯誤,但對於一些二進制文件,就會致使瀏覽器解碼出錯。爲了萬無一失,服務端應該徹底作到向客戶端返回原生二進制流,也就是字節數組。具體的解碼應由瀏覽器進行判斷並實行。緩存

public class ReadClasspathFile {

    private static WeakHashMap<String, byte[]> map = new WeakHashMap<>();

    public static byte[] read(String classPath) {
        //考慮到數據的一致性,這裏沒有使用map的containsKey()
        byte[] s = map.get(classPath);
        if (s != null) {
            return s;
        }
        //判空
        ClassPathResource resource = new ClassPathResource(classPath);
        if (!resource.exists()) {
            return null;
        }
        //讀取
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        try (BufferedInputStream bufferedInputStream = new BufferedInputStream(resource.getInputStream());
             BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(stream)) {
            byte[] bytes = new byte[1024];
            int n;
            while ((n = bufferedInputStream.read(bytes))!=-1){
                bufferedOutputStream.write(bytes,0,n);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //DCL雙檢查鎖
        if (!map.containsKey(classPath)) {
            synchronized (ReadClasspathFile.class) {
                if (!map.containsKey(classPath)) {
                    map.put(classPath, stream.toByteArray());
                }
            }
        }
        return stream.toByteArray();
    }
}

自定義映射

接下來就是Controller層進行映射匹配響應了,這裏利用Spring MVC取個巧,代碼以下:app

@ResponseBody
    @RequestMapping(value = "view/{path}.html",produces = {"text/html; charset=UTF-8"})
    public String view_html(@PathVariable String path) throws IOException {
        return ReadClasspathFile.read("view/"+path+".html");
    }

    @ResponseBody
    @RequestMapping(value = "view/{path}.js",produces = {"application/x-javascript; charset=UTF-8"})
    public String view_js(@PathVariable String path) throws IOException {
        return ReadClasspathFile.read("view/"+path+".js");
    }

    @ResponseBody
    @RequestMapping(value = "view/{path}.css",produces = {"text/css; charset=UTF-8"})
    public String view_html(@PathVariable String path) throws IOException {
        return ReadClasspathFile.read("view/"+path+".css");
    }

經過後戳(html、js)進行判斷,以應對不一樣的Content-Type類型,靜態資源的位置也顯而易見,位於resources/view下。優化

可是,使用@PathVariable註解的這種方式不支持多級路徑,也就是不支持包含「/」,爲了支持匹配多級目錄,咱們只能放棄這種方案,使用另外一種方案。ui

@ResponseBody
    @RequestMapping(value = "/view/**",method = RequestMethod.GET)
    public void view_js(HttpServletResponse response, HttpServletRequest request) throws IOException {
        String uri = request.getRequestURI().trim();
        if (uri.endsWith(".js")){
            response.setContentType("application/javascript");
        }else if (uri.endsWith(".css")){
            response.setContentType("text/css");
        }else if (uri.endsWith(".ttf")||uri.endsWith(".woff")){
            response.setContentType("application/octet-stream");
        }else {
            String contentType = new MimetypesFileTypeMap().getContentType(uri);
            response.setContentType(contentType);
        }
        response.getWriter().print(ReadClasspathFile.read(uri));
    }

將讀取文件的靜態方法更換爲咱們最新的返回字節流的方法,最終代碼爲:

@RequestMapping(value = "/tree/**",method = RequestMethod.GET)
    public void view_js(HttpServletResponse response, HttpServletRequest request) throws IOException {
        String uri = request.getRequestURI().trim();
        if (uri.endsWith(".js")){
            response.setContentType("application/javascript");
        }else if (uri.endsWith(".css")){
            response.setContentType("text/css");
        }else if (uri.endsWith(".woff")){
            response.setContentType("application/x-font-woff");
        }else if (uri.endsWith(".ttf")){
            response.setContentType("application/x-font-truetype");
        }else if (uri.endsWith(".html")){
            response.setContentType("text/html");
        }
        byte[] s = ReadClasspathFile.read(uri);
        response.getOutputStream().write(Optional.ofNullable(s).orElse("404".getBytes()));
    }
相關文章
相關標籤/搜索