[TOC]
提供通用的树形结构定义。
如下图所示,通用树的主体部分包含树(Tree)、树节点(TreeNode)及树节点集合(TreeNodeCollection)三部分内容。
其中:
Tree,是通用树组件直接对外呈现的接口,调用方可通过该接口中定义的create静态方法创建通用树实例。TreeNode,表示树中的节点,可通过get、set方法来获取或设置节点表述的值。TreeNodeCollection,表示TreeNode的集合,在Tree和TreeNode中使用。当在树中使用时,表示树中根节点的集合;在TreeNode中使用时,表示节点中包含子节点的集合。
在整体上,Tree中通过roots方法来定义树中包含的所有根节点,其中每个根节点是一个TreeNode实例。TreeNode本身通过parent和children方法来形成最终的树形结构。
同时,Tree中封装了一系列方法,用以提供基于路径的操作方法,以便于调用方使用。
import modelengine.fitframework.model.tree.Tree;
import modelengine.fitframework.model.tree.TreeNode;
public class Demo {
public static void main(String[] args) {
Tree<Boolean> tree = Tree.create('.');
tree.put("modelengine.fit", true);
tree.put("modelengine.fitframework.demo", true);
print(tree);
// modelengine=null
// fit=true
// fitframework=null
// demo=true
}
private static void print(Tree<?> tree) {
tree.dfs(node -> {
TreeNode<?> parent = node.parent();
while (parent != null) {
System.out.print(" ");
parent = parent.parent();
}
System.out.printf("%s=%s%n", node.name(), node.get());
});
}
}Resource用以定义一个资源,这个资源可以用来描述ClassPath中包含的嵌入资源,也可以用以描述一个外部资源。
hide empty members
skinparam backgroundColor #EEEBDC
skinparam roundcorner 20
skinparam sequenceArrowThickness 2
skinparam ClassFontName Consolas
interface ResourceTree {
+ {abstract} location() : URL
+ {abstract} roots() : <color:blue><u>ResourceTree$NodeCollection</u></color>
+ {abstract} traverse(Consumer<<color:blue><u>ResourceTree$FileNode</u></color>>) : void
+ {abstract} lookup(String[]) : <color:blue><u>ResourceTree$Node</u></color>
+ {static} fromUrl(URL) : <color:blue><u>ResourceTree</u></color>
+ {static} fromJar(<color:blue><u>Jar</u></color>) : <color:blue><u>ResourceTree</u></color>
}
interface ResourceTree$NodeCollection {
+ {abstract} count() : int
+ {abstract} get(String) : <color:blue><u>ResourceTree:Node</u></color>
+ {abstract} get(String, Class) : <color:blue><u>ResourceTree:Node</u></color>
+ {abstract} get(int) : <color:blue><u>ResourceTree:Node</u></color>
+ {abstract} stream() : Stream<<color:blue><u>ResourceTree:Node</u></color>>
+ {abstract} toList() : List<<color:blue><u>ResourceTree:Node</u></color>>
}
ResourceTree *-right-> ResourceTree$NodeCollection : has
interface ResourceTree$Node {
+ {abstract} name() : String
+ {abstract} path() : String
+ {abstract} directory() : <color:blue><u>ResourceTree$DirectoryNode</u></color>
}
ResourceTree$NodeCollection *-right-> ResourceTree$Node : contains
interface ResourceTree$DirectoryNode {
+ {abstract} children() : <color:blue><u>ResourceTree:NodeCollection</u></color>
+ {abstract} traverse(Consumer<<color:blue><u>ResourceTree:Node</u></color>>) : void
+ {abstract} traverse(Predicate<<color:blue><u>ResourceTree:Node</u></color>>, Consumer<<color:blue><u>ResourceTree:Node</u></color>>) : void
}
ResourceTree$DirectoryNode -up-|> ResourceTree$Node
ResourceTree$DirectoryNode *-up-> ResourceTree$NodeCollection : has
ResourceTree$Node ..> ResourceTree$DirectoryNode : refers
interface ResourceTree$FileNode {
+ {abstract} url() : URL
+ {abstract} read() : InputStream
}
ResourceTree$FileNode -up-|> ResourceTree$Node
interface ClassPath {
+ {abstract} loader() : ClassLoader
+ {abstract} url() : URL
+ {abstract} resources() : Tree
+ {abstract} resolve(Pattern<String>) : List<<color:blue><u>ClassPathResource</u></color>>
+ {static} fromClassLoader(ClassLoader) : List<<color:blue><u>ClassPath</u></color>>
}
note top on link
A <color:blue><b>ClassPath</b></color> stores its contained
<color:blue><b>Resource</b></color>s in a tree structure that
can be visited by <color:blue><b>resources()</b></color>
method. Any <color:blue><b>Resource</b></color> is stored
in property <color:blue><b>tag()</b></color> of <color:blue><b>TreeNode</b></color>.
The property may returns null if no
<color:blue><b>Resource</b></color> related to the node.
end note
abstract AbstractClassPath {
- loader : ClassLoader
- url : URL
+ AbstractClassPath(ClassLoader, URL)
+ loader() : ClassLoader
+ url() : URL
+ resolve(Pattern<String>) : List<<color:blue><u>ClassPathResource</u></color>>
+ equals(Object) : boolean
+ hashCode() : int
+ toString() : String
}
AbstractClassPath .up.|> ClassPath
class DirectoryClassPath {
- root : File
- resources : Tree
+ DirectoryClassPath(ClassLoader, URL, File)
+ resources() : Tree
}
DirectoryClassPath -up-|> AbstractClassPath
class JarClassPath {
- resources : Tree
+ JarClassPath(ClassLoader, URL, JarLocation)
+ resources() : Tree
}
JarClassPath -up-|> AbstractClassPath
interface ResourceResolver {
+ {abstract} resolve(String) : <color:blue><u>Resource</u></color>[]
}
class ClassLoaderResourceResolver {
- loader : ClassLoader
- classPaths : List<<color:blue><u>ClassPath</u></color>>
+ ClassPathResourceResolver(ClassLoader)
+ resolve(String) : <color:blue><u>Resource</u></color>[]
}
ClassLoaderResourceResolver .up.|> ResourceResolver
ClassLoaderResourceResolver *-> ClassPath
note top on link
A <color:blue><b>ResourceResolver</b></color> resolve
<color:blue><b>Resource</b></color>s by <color:blue><b>ClassPath</b></color>s.
end note
interface ClassScanner<<(F,#BF00BF)>> {
+ {abstract} scan(Consumer<<color:blue><u>ClassDeclaration</u></color>>) : void
+ {static} custom() : <color:blue><u>ClassScanner$Builder</u></color>
}
interface ClassScanner$Builder {
+ {abstract} loader(ClassLoader) : <color:blue><u>ClassScanner$Builder</u></color>
+ {abstract} basePackages(String[]) : <color:blue><u>ClassScanner$Builder</u></color>
+ {abstract} filter(Predicate<<color:blue><u>ClassDeclaration</u></color>>) : <color:blue><u>ClassScanner$Builder</u></color>
+ {abstract} build() : <color:blue><u>ClassScanner</u></color>
}
ClassScanner$Builder .right.> ClassScanner : instantiate
class DefaultClassScanner {
- classLoader : ClassLoader
- basePackages : Set<String>
- filter : Predicate<<color:blue><u>ClassDeclaration</u></color>>
+ DefaultClassScanner(ClassLoader, String[], Predicate<<color:blue><u>ClassDeclaration</u></color>>)
+ scan(Consumer<<color:blue><u>ClassDeclaration</u></color>>) : void
}
DefaultClassScanner .up.|> ClassScanner
DefaultClassScanner *-down-> ClassPath
资源匹配过程通过给定的字符串判断指定资源是否符合要求。
- 资源路径,用以描述一个资源的全限定名
- 路径节点,资源路径以分隔符
/可分隔成为的若干个部分,其中每个部分为一个路径节点
资源匹配过程可通过通配符匹配多个资源。支持的通配符有:
*,表示在路径节点名称中匹配 0 个或多个任意字符**,表示在资源路径中匹配 0 个或多个任意路径节点
当存在连续多个
*时,多余的*将被折叠。当一个路径节点中包含连续的多个
*时,若这个节点的所有字符都为*,那么则认为其需要匹配任意节点。否则匹配任意字符。例如:
****将被折叠为**x***将被折叠为x*
- 字符串模式匹配
import modelengine.fitframework.util.wildcard.CharSequencePattern;
import modelengine.fitframework.util.wildcard.Pattern;
public class Main {
public static void main(String[] args) {
// pattern 使用 ? 匹配单个字符,使用 * 匹配多个字符
CharSequencePattern pattern = Pattern.forCharSequence("h*l?");
boolean ret = pattern.matches("hello");
System.out.println(ret); // 输出:true
}
}- 路径模式匹配
import modelengine.fitframework.util.StringUtils;
import modelengine.fitframework.util.wildcard.Pattern;
import modelengine.fitframework.util.wildcard.SymbolSequence;
import modelengine.fitframework.util.wildcard.SymbolType;
public class Main {
public static void main(String[] args) {
Pattern<String> pattern = Pattern.custom()
.pattern(SymbolSequence.fromArray(new String[] {"opt", "**", "fit.yaml"}))
.symbol()
.classifier(symbol -> {
if ("**".equals(symbol)) {
return SymbolType.MULTIPLE_WILDCARD;
} else {
return SymbolType.NORMAL;
}
}) // 当为 ** 时,匹配任意数量的路径
.symbol()
.matcher(StringUtils::equalsIgnoreCase) // 忽略大小写匹配
.build();
SymbolSequence<String> value = SymbolSequence.fromArray(new String[] {"opt", "fit", "framework", "FIT.yaml"});
boolean ret = pattern.matches(value);
System.out.println(ret); // 输出:true
}
}- 树形结构匹配
import modelengine.fitframework.util.wildcard.Pattern;
import modelengine.fitframework.util.wildcard.SymbolSequence;
import modelengine.fitframework.util.wildcard.SymbolType;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
public class Main {
public static void main(String[] args) throws IOException {
Pattern<String> pattern = Pattern.custom()
.pattern(SymbolSequence.fromArray(new String[] {"opt", "**", "fit.yaml"}))
.symbol()
.classifier(symbol -> {
if ("**".equals(symbol)) {
return SymbolType.MULTIPLE_WILDCARD;
} else {
return SymbolType.NORMAL;
}
}) // 当为 ** 时,匹配任意数量的路径
.symbol()
.matcher(StringUtils::equalsIgnoreCase) // 忽略大小写匹配
.build();
List<File> roots = Collections.singletonList(new File("/opt").getCanonicalFile());
Function<File, List<File>> childrenMapper =
file -> Optional.ofNullable(file.listFiles()).map(Arrays::asList).orElse(Collections.emptyList());
Function<File, String> symbolMapper = File::getName;
List<File> matched = pattern.match(roots, childrenMapper, symbolMapper);
for (File file : matched) {
System.out.println(file.getPath());
}
}
}JRE在解析URL时,需要去获取对应协议的URLStreamHandler实例,来对URL进行处理。在扩展Jar In Jar能力时,我们需要重写JRE对jar协议的支持。
即实现自己的针对jar协议的URLStreamHandler,并使其在URL解析过程中生效。
扩展URL支持的协议有多种方式,但是较为通用的方法为通过java.protocol.handler.pkgs系统属性,主要的原因是这种扩展方式被支持的较早,所支持的JDK版本更多。
在通过java.protocol.handler.pkgs系统属性来获取URLStreamHandler时,主要过程为:
- 获取
java.protocol.handler.pkgs系统属性的值,并使用|字符进行分割。 - 分割后的每个元素被认为是一个
package名,在该包下的协议名称的子包中查找名为Handler的类型 - 通过默认构造方法实例化
Handler类,并将其强转为URLStreamHandler使用
JDK预置的URLStreamHandler都定义在sun.net.www.protocol包下,以其作为参考,我们将使用modelengine.fitframework.protocol包作为对URL协议的支持。
因此,我们将通过modelengine.fitframework.protocol.jar.Handler类型,提供对jar协议的扩展。
在进行ResourceResolver的性能优化时,发现了一些问题:
Jar In Jar解析过程耗时较多,其原因集中在读取JAR文件数据的过程中。
原始的实现逻辑中最终通过同一个DataBlock来对文件的数据进行读取,为了应对并发访问的问题,在每次读取时都会对seek和read操作进行加锁,以按预期读取到文件中指定位置的数据。在此过程中,即使只有单线程读取,也会在每次读取时进行入锁和出锁操作,性能较差。
为了解决该问题,决定在每次需要对一个内容进行数据读取时,使用独立的对象进行,以避免并发访问过程的相互影响,从而避免并发控制导致的性能损耗。
同时,因为不同内容的读取过程使用了独立了IO对象,因此也就没有必要在根JAR中保持一个用以读取的DataBlock来承载所有内容的读取工作,因此同时将Jar和JarEntry进行托管处理,消除其中所持有的非托管资源。将非托管资源的建立汇聚到相应的实例方法调用中。
优化后的类图如下:
hide empty members
skinparam backgroundColor #EEEBDC
skinparam roundcorner 20
skinparam sequenceArrowThickness 2
skinparam ClassFontName Consolas
interface Jar {
+ {abstract} url() : URL
+ {abstract} permission() : Permission
+ {abstract} entries() : <color:blue><u>Jar$EntryCollection</u></color>
+ {abstract} comment() : String
+ {static} load(File) : <color:blue><u>Jar</u></color>
}
interface Jar$EntryCollection {
+ {abstract} size() : int
+ {abstract} get(int) : <color:blue><u>Jar$Entry</u></color>
+ {abstract} get(String) : <color:blue><u>Jar$Entry</u></color>
}
Jar *-right-> Jar$EntryCollection
interface Jar$Entry {
+ {abstract} jar() : Jar
+ {abstract} name() : String
+ {abstract} methodOfCompression() : <color:blue><u>CompressionMethod</u></color>
+ {abstract} crc32() : int
+ {abstract} offset() : long
+ {abstract} sizeOfCompressed() : long
+ {abstract} sizeOfUncompressed() : long
+ {abstract} timeOfLastModification() : Date
+ {abstract} extra() : byte[]
+ {abstract} comment() : String
+ {abstract} directory() : boolean
+ {abstract} asJar() : <color:blue><u>NestedJar</u></color>
}
Jar$EntryCollection *-right-> Jar$Entry
enum CompressionMethod {
+ {static} NONE
+ {static} DEFLATED
}
Jar$Entry .right.> CompressionMethod : with
interface NestedJar {
+ {abstract} entry() : <color:blue><u>Jar$Entry</u></color>
+ {abstract} parent() : <color:blue><u>Jar</u></color>
}
NestedJar -right-|> Jar
abstract AbstractJar {
- location : <color:blue><u>DataLocation</u></color>
- comment : String
- entries : <color:blue><u>DefaultJarEntryCollection</u></color>
+ AbstractJar(<color:blue><u>DataLocation</u></color>)
location() : <color:blue><u>DataLocation</u></color>
+ file() : File
+ entries() : <color:blue><u>Jar$EntryCollection</u></color>
+ comment() : String
+ url() : URL
}
AbstractJar .up.|> Jar
package ".ZIP File Format Specification" as ZIP {
class EndOfCentralDirectory {
- offsetOfCentralDirectory : long
- sizeOfCentralDirectory : long
- numberOfEntries : long
- commentOfZipFile : String
- EndOfCentralDirectory(long, long, long, String)
+ offsetOfCentralDirectory() : long
+ sizeOfCentralDirectory() : long
+ numberOfEntries() : long
+ commentOfZipFile() : String
+ {static} load(FileRandomAccessor) : <color:blue><u>EndOfCentralDirectory</u></color>
}
AbstractJar .down.> EndOfCentralDirectory : use
class CentralDirectory {
- access : FileRandomAccessor
- offsetOfCentralDirectory : long
- sizeOfCentralDirectory : long
- numberOfEntries : int
- CentralDirectory(FileRandomAccessor, long, long, int)
+ enumerator() : Enumerator<<color:blue><u>CentralDirectoryFileHeader</u></color>>
+ {static} of(<color:blue><u>EndOfCentralDirectory</u></color>, FileRandomAccessor) : <color:blue><u>CentralDirectory></u></color>
}
AbstractJar .down.> CentralDirectory : use
class CentralDirectoryFileHeader {
- sizeOfCompressed : long
- sizeOfUncompressed : long
- offsetOfLocalHeader : long
- methodOfCompression : int
- crc32 : int
- filename : String
- extra : byte[]
- comment : String
- size : int
+ CentralDirectoryFileHeader(long, long, long, int,\n int, MsDosDateTime, String, byte[], String, int)
+ sizeOfCompressed() : long
+ sizeOfUncompressed() : long
+ offsetOfLocalHeader() : long
+ methodOfCompression() : int
+ crc32() : int
+ filename() : String
+ extra() : byte[]
+ comment() : String
+ size() : int
}
AbstractJar .down.> CentralDirectoryFileHeader : use
CentralDirectory .> CentralDirectoryFileHeader : enumerate
class LocalFileHeader {
- size : long
- LocalFileHeader(long)
+ size() : long
+ {static} load(FileRandomAccessor, long) : <color:blue><u>LocalFileHeader</u></color>
}
AbstractJar .down.> LocalFileHeader : use
EndOfCentralDirectory -[hidden]> CentralDirectory
}
class RootJar {
- permission : Permission
+ RootJar(File)
+ permission() : Permission
}
RootJar -left-|> AbstractJar
class NestedFileJar {
- entry : <color:blue><u>Jar$Entry</u></color>
+ NestedFileJar(<color:blue><u>DefaultJarEntry</u></color>)
+ permission() : Permission
+ entry() : <color:blue><u>Jar$Entry</u></color>
+ parent() : <color:blue><u>Jar</u></color>
}
NestedFileJar -right-|> AbstractJar
NestedFileJar .up.|> NestedJar
class NestedDirectoryJar {
- entry : <color:blue><u>Jar$Entry</u></color>
- entries : <color:blue><u>DefaultJarEntryCollection</u></color>
+ NestedDirectoryJar(<color:blue><u>Jar$Entry</u></color>)
+ entry() : <color:blue><u>Jar$Entry</u></color>
+ parent() : <color:blue><u>Jar</u></color>
+ file() : File
+ permission() : Permission
+ entries() : <color:blue><u>Jar$EntryCollection</u></color>
+ comment() : String
+ url() : URL
+ toString() : String
}
NestedDirectoryJar .up.|> NestedJar
NestedDirectoryJar -[hidden]> NestedFileJar
class DefaultJarEntryCollection {
- keys : List<String>
- entries : Map<String, <color:blue><u>Jar$Entry</u></color>>
+ DefaultJarEntryCollection(int)
add(<color:blue><u>Jar$Entry</u></color>) : void
+ size() : int
+ get(int) : <color:blue><u>Jar$Entry</u></color>
+ get(String) : <color:blue><u>Jar$Entry</u></color>
+ stream() : Stream<<color:blue><u>Jar$Entry</u></color>>
+ iterator() : Iterator<<color:blue><u>Jar$Entry</u></color>>
+ toString() : String
}
DefaultJarEntryCollection .down.|> Jar$EntryCollection
class DefaultJarEntry {
- jar : <color:blue><u>Jar</u></color>
- name : String
- compressionMethod : <color:blue><u>CompressionMethod</u></color>
- crc32 : int
- sizeOfCompressed : long
- sizeOfUncompressed : long
- lastModificationTime : Date
- extra : byte[]
- comment : String
- offset : long
- location: <color:blue><u>DataLocation</u></color>
+ DefaultJarEntry(Jar, String, CompressionMethod, int,\n long, long, Date, byte[], String, long, DataLocation)
location() : <color:blue><u>DataLocation</u></color>
+ jar() : <color:blue><u>Jar</u></color>
+ name() : String
+ methodOfCompression() : <color:blue><u>CompressionMethod</u></color>
+ crc32() : int
+ offset() : long
+ sizeOfCompressed() : long
+ sizeOfUncompressed() : long
+ timeOfLastModification() : Date
+ extra() : byte[]
+ comment() : String
+ directory() : boolean
+ asJar() : <color:blue><u>NestedJar</u></color>
}
DefaultJarEntry .down.|> Jar$Entry
note right of DefaultJarEntry::"asJar()"
When <color:blue><i>directory()</i></color> is true, then return
an instance of <color:blue><b>NestedDirectoryJar</b></color>.
Otherwise return an instance of
<color:blue><b>NestedFileJar</b></color>.
end notehide empty members
skinparam backgroundColor #EEEBDC
skinparam roundcorner 20
skinparam sequenceArrowThickness 2
skinparam ClassFontName Consolas
interface ClassScanner {
+ {abstract} scan(String[]) : void
+ {static} create(Consumer<<color:blue><u>ClassDeclaration</u></color>>) : <color:blue><u>ClassScanner</u></color>
}
interface ClassScanner$Callback {
+ {abstract} onClassDetected(<color:blue><u>ClassScanner</u></color>, <color:blue><u>ClassDeclaration</u></color>) : void
}
ClassScanner .right.> ClassScanner$Callback : callback
interface ClassScanner$Builder {
+ {abstract} addClassLoader(ClassLoader) : <color:blue><u>ClassScanner$Builder</u></color>
+ {abstract} addCallback(<color:blue><u>ClassScanner$Callback</u></color>) : <color:blue><u>ClassScanner$Builder</u></color>
+ {abstract} build() : <color:blue><u>ClassScanner</u></color>
}
ClassScanner$Builder .right.> ClassScanner : create
class DefaultClassScanner {
- scanners : Map<URL, <color:blue><u>ClassPathScanner</u></color>>
+ DefaultClassScanner(ClassLoader[], <color:blue><u>ClassScanner$Callback</u></color>)
+ scan(String[]) : void
}
DefaultClassScanner .up.|> ClassScanner
abstract ClassPathScanner {
- classPath : URL
+ ClassPathScanner(URL)
+ {abstract} scan(<color:blue><u>ClassScanner$Callback</u></color>) :
+ {static} fromUrl(URL) : <color:blue><u>ClassPathScanner</u></color>
}
DefaultClassScanner *-> ClassPathScanner : each url
class DirectoryClassPathScanner {
}
DirectoryClassPathScanner -up-|> ClassPathScanner
class JarClassPathScanner {
}
JarClassPathScanner -up-|> ClassPathScanner