浏览代码

添加路由扫描功能

James 5 年之前
父节点
当前提交
940f1bb2b8

+ 66 - 0
src/main/java/com/jfinal/config/Routes.java

@@ -18,9 +18,11 @@ package com.jfinal.config;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 import com.jfinal.aop.Interceptor;
 import com.jfinal.aop.InterceptorManager;
 import com.jfinal.core.Controller;
+import com.jfinal.core.PathScanner;
 import com.jfinal.kit.StrKit;
 
 /**
@@ -59,6 +61,70 @@ public abstract class Routes {
 	}
 	
 	/**
+	 * 扫描路由
+	 * 
+	 * <pre>
+	   1:路由不拆分例子:
+	     routes.setBaseViewPath("/_view");
+	     routes.scan("com.jfinal.club.");
+	   
+	   2:前后台路由拆分例子(例子来源于俱乐部项目源码 jfinal-club):
+		// 扫描后台路由
+		me.add(new Routes() {
+			public void config() {
+				// 添加后台管理拦截器,将拦截在此方法中注册的所有 Controller
+				addInterceptor(new AdminAuthInterceptor());
+				addInterceptor(new PjaxInterceptor());
+				
+				setBaseViewPath("/_view/_admin");
+				
+				// 如果被扫描的包在 jar 文件之中,需要添加如下配置:
+				// undertow.hotSwapClassPrefix = com.jfinal.club._admin.
+				scan("com.jfinal.club._admin.");
+			}
+		});
+		
+		
+		// 扫描前台路由
+		me.add(new Routes() {
+			public void config() {
+				setBaseViewPath("/_view");
+				
+				// 如果被扫描的包在 jar 文件之中,需要添加如下配置:
+				// undertow.hotSwapClassPrefix = com.jfinal.club.
+				scan("com.jfinal.club.", className -> {
+					// className 为当前正扫描的类名,返回 true 时表示过滤掉不扫描当前类
+					return className.startsWith("com.jfinal.club._admin.");
+				});
+			}
+		});
+		
+		注意:
+		1:拆分路由是为了可以独立配置 setBaseViewPath(...)、addInterceptor(...)
+		2:scan(...) 方法要添加过滤,过滤掉后台路由,否则后台路由会被扫描到,
+		   造成 baseViewPath 以及 routes 级别的拦截器配置错误
+		3: 由于 scan(...) 内部避免了重复扫描同一个类,所以需要将扫描前台路由代码
+		   放在扫描后台路由之前才能验证没有过滤造成的后果
+		
+	 * </pre>
+	 * 
+	 * @param basePackage 进行扫描的基础 package,仅扫描该包及其子包下面的路由
+	 * @param filter 用于过滤不需要被扫描的目标
+	 */
+	public Routes scan(String basePackage, Predicate<String> filter) {
+		new PathScanner(basePackage, this, filter).scan();
+		return this;
+	}
+	
+	/**
+	 * 扫描路由
+	 * @param basePackage 进行扫描的基础 package,仅扫描该包及其子包下面的路由
+	 */
+	public Routes scan(String basePackage) {
+		return scan(basePackage, null);
+	}
+	
+	/**
 	 * Add Routes
 	 */
 	public Routes add(Routes routes) {

+ 38 - 0
src/main/java/com/jfinal/core/Path.java

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2011-2021, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.core;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Path 注解用于配置 Controller 的 controllerPath 以及 viewPath
+ * 搭配 PathScanner 实现路由扫描功能
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface Path {
+	String value();
+	String viewPath() default "";
+}
+

+ 238 - 0
src/main/java/com/jfinal/core/PathScanner.java

@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2011-2021, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import com.jfinal.config.Routes;
+import com.jfinal.kit.StrKit;
+
+/**
+ * PathScanner 扫描 @Path 注解,实现路由扫描功能
+ */
+public class PathScanner {
+	
+	// 存放已被扫描过的 controller,避免被多次扫描
+	private static final Set<Class<?>> scannedController = new HashSet<>();
+	
+	// 扫描的基础 package,只扫描该包及其子包之下的类
+	private String basePackage;
+	
+	// 过滤不需要被扫描的类
+	private Predicate<String> filter;
+	
+	// 调用 Routes.add(...) 添加扫描结果
+	private Routes routes;
+	
+	private ClassLoader classLoader;
+	
+	public PathScanner(String basePackage, Routes routes, Predicate<String> filter) {
+		if (StrKit.isBlank(basePackage)) {
+			throw new IllegalArgumentException("basePackage can not be blank");
+		}
+		if (routes == null) {
+			throw new IllegalArgumentException("routes can not be null");
+		}
+		
+		String bp = basePackage.replace('.', '/');
+		bp = bp.endsWith("/") ? bp : bp + '/';				// 添加后缀字符 '/'
+		bp = bp.startsWith("/") ? bp.substring(1) : bp;		// 删除前缀字符 '/'
+		
+		this.basePackage = bp;
+		this.routes = routes;
+		this.filter = filter;
+	}
+	
+	public PathScanner(String basePackage, Routes routes) {
+		this(basePackage, routes, null);
+	}
+	
+	public void scan() {
+		try {
+			classLoader = getClassLoader();
+			List<URL> urlList = getResources();
+			scanResources(urlList);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+	
+	private ClassLoader getClassLoader() {
+		ClassLoader ret = Thread.currentThread().getContextClassLoader();
+		return ret != null ? ret : PathScanner.class.getClassLoader();
+	}
+	
+	private List<URL> getResources() throws IOException {
+		List<URL> ret = new ArrayList<>();
+		
+		Set<String> urlSet = new HashSet<>();			// 用于去除重复
+		Enumeration<URL> urls = getClassLoader().getResources(basePackage);
+		while (urls.hasMoreElements()) {
+			URL url = urls.nextElement();
+			if ( ! urlSet.contains(url.toString()) ) {
+				urlSet.add(url.toString());
+				ret.add(url);
+			}
+		}
+		return ret;
+	}
+	
+	private void scanResources(List<URL> urlList) throws IOException {
+		for (URL url : urlList) {
+			String protocol = url.getProtocol();
+			if ("jar".equals(protocol)) {
+				scanJar(url);
+			} else if ("file".equals(protocol)) {
+				scanFile(url);
+			}
+		}
+	}
+	
+	private void scanJar(URL url) throws IOException {
+		JarFile jarFile = null;
+		try {
+			URLConnection urlConn = url.openConnection();
+			if (urlConn instanceof JarURLConnection) {
+				JarURLConnection jarUrlConn = (JarURLConnection)urlConn;
+				jarFile = jarUrlConn.getJarFile();
+				
+				Enumeration<JarEntry> jarFileEntries = jarFile.entries();
+				while (jarFileEntries.hasMoreElements()) {
+					JarEntry je = jarFileEntries.nextElement();
+					String en = je.getName();
+					// 只扫描 basePackage 之下的类
+					if (en.endsWith(".class") && en.startsWith(basePackage)) {
+						en = en.substring(0, en.length() - 6).replace(File.separatorChar, '.');
+						scanController(en);
+					}
+				}
+			}
+		} finally {
+			if (jarFile != null) {
+				jarFile.close();
+			}
+		}
+	}
+	
+	private void scanFile(URL url) {
+		String path = url.getPath();
+		path = decodeUrl(path);
+		File file = new File(path);
+		String classPath = getClassPath(file);
+		scanFile(file, classPath);
+	}
+	
+	private void scanFile(File file, String classPath) {
+		if (file.isDirectory()) {
+			File[] files = file.listFiles();
+			if (files != null) {
+				for (File fi : files) {
+					scanFile(fi, classPath);
+				}
+			}
+		}
+		else if (file.isFile()) {
+			String fullName = file.getAbsolutePath();
+			if (fullName != null && fullName.endsWith(".class")) {
+				String className = fullName.substring(classPath.length(), fullName.length() - 6).replace(File.separatorChar, '.');
+				scanController(className);
+			}
+		}
+	}
+	
+	private String getClassPath(File file) {
+		// 将 basePackage 中的路径分隔字符转换成与 OS 相同,方便处理路径
+		String bp = basePackage.replace('/', File.separatorChar);
+		
+		String ret = file.getAbsolutePath();
+		
+		// 添加后缀,以便后续的 indexOf(bp) 可以正确获得下标值,因为 bp 确定有后缀
+		if ( ! ret.endsWith(File.separator) ) {
+			ret = ret + File.separator;
+		}
+		
+		int index = ret.indexOf(bp);
+		if (index != 0) {
+			ret = ret.substring(0, index);
+		}
+		
+		return ret.endsWith(File.separator) ? ret : ret + File.separator;
+	}
+	
+	@SuppressWarnings("unchecked")
+	private void scanController(String className) {
+		// 过滤不需要扫描的 className
+		if (filter != null && filter.test(className)) {
+			return ;
+		}
+		
+		Class<?> c = loadClass(className);
+		if (c != null && Controller.class.isAssignableFrom(c) && !scannedController.contains(c)) {
+			// 确保 class 只被扫描一次
+			scannedController.add(c);
+			
+			int mod = c.getModifiers();
+			if (Modifier.isPublic(mod) && ! Modifier.isAbstract(mod)) {
+				Path path = c.getAnnotation(Path.class);
+				if (path != null) {
+					String pa = path.value();
+					String vp = path.viewPath();
+					routes.add(pa, (Class<? extends Controller>)c, vp.length() != 0 ? vp : pa);
+				}
+			}
+		}
+	}
+	
+	private Class<?> loadClass(String className) {
+		try {
+			return classLoader.loadClass(className);
+		} catch (NoClassDefFoundError | UnsupportedClassVersionError e) {
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+		return null;
+	}
+	
+	/**
+	 * 支持路径中存在空格百分号等等字符
+	 */
+	private static String decodeUrl(String url) {
+		try {
+			return URLDecoder.decode(url, "UTF-8");
+		} catch (java.io.UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}
+
+
+
+
+