做爲依賴使用的SpringBoot工程很容易出現自身靜態資源被主工程忽略的狀況。可是做爲依賴而存在的Controller方法卻不會失效,咱們知道,Spring MVC對於靜態資源的處理也不外乎是路徑匹配,讀取資源封裝到Response中響應給瀏覽器,因此,解決的途徑就是本身寫一個讀取Classpath下靜態文件並響應給客戶端的方法。javascript
對於ClassPath下文件的讀取,最容易出現的就是IDE運行ok,打成jar包就沒法訪問了,該問題的緣由仍是在於getResources()不如getResourceAsStream()方法靠譜。css
本就是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())); }