來吧,看看shiro是怎么解析ini文件的,這里假設ini文件在classpath下,名字叫做shiro.ini
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
shiro.ini
[users] zhang=123 wang=123 [main] #指定securityManager的authenticator實現 authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator securityManager.authenticator=$authenticator #指定securityManager.authenticator的authenticationStrategy allSuccessfulStrategy=org.apache.shiro.authc.pam.FirstSuccessfulStrategy securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
一、加載ini配置文件
1 public static InputStream getInputStreamForPath(String resourcePath) throws IOException { 2 3 InputStream is; 4 if (resourcePath.startsWith(CLASSPATH_PREFIX)) {//判斷是否為classpath:開頭的 5 is = loadFromClassPath(stripPrefix(resourcePath)); 6 7 } else if (resourcePath.startsWith(URL_PREFIX)) {//判斷是否為url:開頭 8 is = loadFromUrl(stripPrefix(resourcePath)); 9 10 } else if (resourcePath.startsWith(FILE_PREFIX)) {//判斷是否為file:開頭 11 is = loadFromFile(stripPrefix(resourcePath)); 12 13 } else { 14 is = loadFromFile(resourcePath); 15 } 16 17 if (is == null) { 18 throw new IOException("Resource [" + resourcePath + "] could not be found."); 19 } 20 21 return is; 22 }
上面的代碼中對我們傳進來的配置文件進行前綴判斷,再以相應的方法取加載它
stripPrefix(resourcePath)是去掉前綴,那么傳進去的classpath:shiro.ini就變成shiro.ini了,下面就是加載配置文件的方法
1 public static InputStream getResourceAsStream(String name) { 2 3 InputStream is = THREAD_CL_ACCESSOR.getResourceStream(name); 4 5 if (is == null) { 6 if (log.isTraceEnabled()) { 7 log.trace("Resource [" + name + "] was not found via the thread context ClassLoader. Trying the " + 8 "current ClassLoader..."); 9 } 10 is = CLASS_CL_ACCESSOR.getResourceStream(name); 11 } 12 13 if (is == null) { 14 if (log.isTraceEnabled()) { 15 log.trace("Resource [" + name + "] was not found via the current class loader. Trying the " + 16 "system/application ClassLoader..."); 17 } 18 is = SYSTEM_CL_ACCESSOR.getResourceStream(name); 19 } 20 21 if (is == null && log.isTraceEnabled()) { 22 log.trace("Resource [" + name + "] was not found via the thread context, current, or " + 23 "system/application ClassLoaders. All heuristics have been exhausted. Returning null."); 24 } 25 26 return is; 27 }
加載配置文件的時候,首先使用了線程的上下文加載器,如果加載不到就用類加載器,下面是這些加載器的獲取代碼
private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override protected ClassLoader doGetClassLoader() throws Throwable { return Thread.currentThread().getContextClassLoader(); } }; /** * @since 1.0 */ private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override protected ClassLoader doGetClassLoader() throws Throwable { return ClassUtils.class.getClassLoader(); } }; /** * @since 1.0 */ private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override protected ClassLoader doGetClassLoader() throws Throwable { return ClassLoader.getSystemClassLoader(); } };
當獲取到配置文件的輸入流后,使用了isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);這行代碼就輸入流變成了字節輸入流隨后調用過了load(isr)方法
1 public void load(Reader reader) { 2 Scanner scanner = new Scanner(reader); 3 try { 4 load(scanner); 5 } finally { 6 try { 7 scanner.close(); 8 } catch (Exception e) { 9 log.debug("Unable to cleanly close the InputStream scanner. Non-critical - ignoring.", e); 10 } 11 } 12 }
上面使用了Scanner類對reader進行了包裝,隨后有調用了load(scanner);
1 public void load(Scanner scanner) { 2 3 String sectionName = DEFAULT_SECTION_NAME;//默認節點名稱為空字符串 4 StringBuilder sectionContent = new StringBuilder();//用於保存節點的內容 5 6 while (scanner.hasNextLine()) { 7 8 String rawLine = scanner.nextLine();//讀取一行數據 9 String line = StringUtils.clean(rawLine);//去除字符串的兩邊的空白字符,如果這個字符是空字符串,那么返回null 10 11 if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {//判斷這行數據是否為null,或者是以#或者是;開頭的注釋. 12 //skip empty lines and comments: 13 continue; 14 } 15 16 String newSectionName = getSectionName(line);//判斷是否為節點名(如[main]這樣的)並且去掉[],如[main] 17 if (newSectionName != null) {//如果節點不為空,那么就添加節點 18 //found a new section - convert the currently buffered one into a Section object 19 addSection(sectionName, sectionContent);//添加節點 20 21 //reset the buffer for the new section: 22 sectionContent = new StringBuilder(); 23 24 sectionName = newSectionName; //保存節點名,在讀取完配置文件后,還得通過它添加節點(第36行代碼需要用到) 25 26 if (log.isDebugEnabled()) { 27 log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX); 28 } 29 } else { 30 //normal line - add it to the existing content buffer: 31 sectionContent.append(rawLine).append("\n");//說名讀取到這行不是節點名,那么就將內容保存到sectionContent中 32 } 33 } 34 35 //finish any remaining buffered content: 36 addSection(sectionName, sectionContent);//讀到文件結尾時添加最后的這個節點 37 }
第19行是添加節點,下面是添加節點的判斷代碼,它首先要確認你這個節點內是否有內容,如果沒有就不添加,這種情況一般發生在
shiro解析第一個節點的時候,比如我這里的ini配置文件,shiro一開頭讀取到是[users]這個節點,到達第17行這條語句的時候,明顯shiro還沒有讀取[users]這個節點內的內容
所以還不能進行添加
1 private void addSection(String name, StringBuilder content) { 2 if (content.length() > 0) { 3 String contentString = content.toString(); 4 String cleaned = StringUtils.clean(contentString); 5 if (cleaned != null) { 6 Section section = new Section(name, contentString); 7 if (!section.isEmpty()) { 8 sections.put(name, section); 9 } 10 } 11 } 12 }
二、節點的添加
接着上面的,當節點內容不為空時,也就是一個節點被完整的讀取出來了,那么就會創建節點對象
1 private void addSection(String name, StringBuilder content) { 2 if (content.length() > 0) { 3 String contentString = content.toString(); 4 String cleaned = StringUtils.clean(contentString); 5 if (cleaned != null) { 6 Section section = new Section(name, contentString); 7 if (!section.isEmpty()) { 8 sections.put(name, section); 9 } 10 } 11 } 12 }
第6行,創建了一個Section對象,這個Section類實現了Map接口,是個map容器,Ini也實現了Map接口是個Map容器,並且Section是Ini的一個嵌套類。
打開Section這個構造器,它傳入了兩個參數,一個是節點名,另一個是這個節點下面的內容,如[users],那么節點內容就是
zhang=123 wang=123
1 private Section(String name, String sectionContent) { 2 if (name == null) { 3 throw new NullPointerException("name"); 4 } 5 this.name = name; 6 Map<String,String> props; 7 if (StringUtils.hasText(sectionContent) ) { 8 props = toMapProps(sectionContent);//將內容解析存到Map中 9 } else { 10 props = new LinkedHashMap<String,String>(); 11 } 12 if ( props != null ) { 13 this.props = props; 14 } else { 15 this.props = new LinkedHashMap<String,String>(); 16 } 17 }
重點看看第8行的內容,這個方法會把
zhang=123 wang=123
解析成鍵值對的形式存到props這個Map里面
實現代碼為
1 private static Map<String, String> toMapProps(String content) { 2 Map<String, String> props = new LinkedHashMap<String, String>(); 3 String line; 4 StringBuilder lineBuffer = new StringBuilder(); 5 Scanner scanner = new Scanner(content); 6 while (scanner.hasNextLine()) { 7 line = StringUtils.clean(scanner.nextLine());//去掉兩邊的空白符,如果本身是個空字符串,那么返回null 8 if (isContinued(line)) {//判斷是否存在反斜杠\,如果存在就繼續讀,反斜杠就像java中的+,表示這些字符串是連在一起的,一行寫不下,放到下一行 9 //strip off the last continuation backslash: 10 line = line.substring(0, line.length() - 1);//去掉反斜杠 11 lineBuffer.append(line); 12 continue; 13 } else { 14 lineBuffer.append(line); 15 } 16 line = lineBuffer.toString(); 17 lineBuffer = new StringBuilder(); 18 String[] kvPair = splitKeyValue(line); 19 props.put(kvPair[0], kvPair[1]); 20 } 21 22 return props; 23 }
這里有兩個比較重點的方法,一個是第8行的isContinued,還有一個是第18行的splitKeyValue方法
首先看下isContinued
1 protected static boolean isContinued(String line) { 2 if (!StringUtils.hasText(line)) { 3 return false; 4 } 5 int length = line.length(); 6 //find the number of backslashes at the end of the line. If an even number, the 7 //backslashes are considered escaped. If an odd number, the line is considered continued on the next line 8 int backslashCount = 0; 9 for (int i = length - 1; i > 0; i--) { 10 if (line.charAt(i) == ESCAPE_TOKEN) {//判斷時候等於反斜杠 11 backslashCount++; 12 } else { 13 break; 14 } 15 } 16 return backslashCount % 2 != 0; 17 }
上面這段代碼的意思是,從一句話的最后開始往前查找反斜杠,如果反斜杠的個數是奇數個,那么就返回true,如果是偶數那么就返回
false,為什么呢?反斜杠在shiro的配置中被認為是轉義字符,比如\\那么表示的\,只有一個\或者奇數個\\\=》表示用戶需要輸出一個\,另一個\就不會轉義,跟java中的反斜杠是
一樣的。
將每條鍵值對信息讀取完整之后,就可以開始進行key,value的解析了
現在來看看splitKeyValue方法
1 protected static String[] splitKeyValue(String keyValueLine) { 2 String line = StringUtils.clean(keyValueLine); 3 if (line == null) { 4 return null; 5 } 6 StringBuilder keyBuffer = new StringBuilder(); 7 StringBuilder valueBuffer = new StringBuilder(); 8 9 boolean buildingKey = true; //we'll build the value next: 10 11 for (int i = 0; i < line.length(); i++) { 12 char c = line.charAt(i);//循環遍歷每個字符 13 14 if (buildingKey) {//這個值為true時,表示對key值進行解析 15 if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {//isKeyValueSeparatorChar是在判斷這個字符是否是:或這=,isCharEscaped表示這個字符前是否存在反斜杠 16 buildingKey = false;//now start building the value 17 } else { 18 keyBuffer.append(c); 19 } 20 } else { 21 if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) { 22 //swallow the separator chars before we start building the value 23 } else { 24 valueBuffer.append(c); 25 } 26 } 27 } 28 29 String key = StringUtils.clean(keyBuffer.toString()); 30 String value = StringUtils.clean(valueBuffer.toString()); 31 32 if (key == null || value == null) { 33 String msg = "Line argument must contain a key and a value. Only one string token was found."; 34 throw new IllegalArgumentException(msg); 35 } 36 37 log.trace("Discovered key/value pair: {}={}", key, value); 38 39 return new String[]{key, value}; 40 }
第15行的isKeyValueSeparatorChar代碼如下
private static boolean isKeyValueSeparatorChar(char c) { return Character.isWhitespace(c) || c == ':' || c == '='; }
isCharEscaped的代碼如下
private static boolean isCharEscaped(CharSequence s, int index) { return index > 0 && s.charAt(index - 1) == ESCAPE_TOKEN;//ESCAPE_TOKEN表示反斜杠 }
為什么要這么判斷,原因很簡單就是像\=和\:都會被轉義
當找到=或者:時,key的解析結束,將buildingKey設置為false,開始解析value,解析value的時候要注意一下第21行的判斷語句
這行判斷語句的意思是,當valueBuffer中沒有值的時候,如果出現=或這:,那么這些字符將被忽略,比如說zhang===:::123,它會忽略掉第一個等號后面的=或者:
如果是這樣的zhang===qwer=rtet,它只會解析到第一個=后面的=不會被解析,綜合以上的判斷方式,最后得出的key是zhang,value是qwer=rtet
解析出key和value后將被存到Section類的props這個Map中
並且最后節點名字和Section對象會被存到Ini了的sections這個Map中sections.put(name, section);
