上一篇博客介绍了使用java动态代理来根据请求的url路径动态的转发到不同的controller来执行不同的逻辑的方法
这一篇来介绍使用java反射的方式实现方法
先看下效果图
说一下我折腾的思路
先上本篇博客用到的依赖
<properties> <undertow.version>2.0.20.Final</undertow.version> </properties> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-core</artifactId> <version>${undertow.version}</version> </dependency> <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-servlet</artifactId> <version>${undertow.version}</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> </dependencies>
原链接文: https://tomoya92.github.io/2019/04/10/mvc-enhancer/
我这里创建了两个注解, @Controller
@GetMapping()
分别对应着spring里的这两个注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface Controller {}
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface Controller { String value() default ""; }
@Controller public class HelloController { @GetMapping("/") public String index(HttpServerExchange exchange, Map<String, Object> model) { // 获取参数 Map<String, Deque<String>> queryParameters = exchange.getQueryParameters(); String name = queryParameters.get("name").getFirst(); // 将参数放进传进来的Map对象里,这个对象还会传给视图解析类参与视图的渲染 model.put("name", name); // 视图的模板文件名 return "index"; } @GetMapping("/about") public String about(HttpServerExchange exchange, Map<String, Object> model) { return "about"; } }
@Controller public class UserController { @GetMapping("/user/list") public String list(HttpServerExchange exchange, Map<String, Object> model) { List<String> users = Arrays.asList("tomcat", "jetty", "undertow"); model.put("users", users); return "user_list"; } }
扫描包这个类不是我写的,在csdn上找的,原文地址已在代码里标注了
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; /** * This scanner is used to find out all classes in a package. * Created by whf on 15-2-26. * * 源码来自:https://blog.csdn.net/neosmith/article/details/43955963 */ public class ClasspathPackageScanner { private Logger logger = LoggerFactory.getLogger(ClasspathPackageScanner.class); private String basePackage; private ClassLoader cl; /** * Construct an instance and specify the base package it should scan. * * @param basePackage The base package to scan. */ public ClasspathPackageScanner(String basePackage) { this.basePackage = basePackage; this.cl = getClass().getClassLoader(); } /** * Construct an instance with base package and class loader. * * @param basePackage The base package to scan. * @param cl Use this class load to locate the package. */ public ClasspathPackageScanner(String basePackage, ClassLoader cl) { this.basePackage = basePackage; this.cl = cl; } /** * Get all fully qualified names located in the specified package * and its sub-package. * * @return A list of fully qualified names. * @throws IOException */ public List<String> getFullyQualifiedClassNameList() throws IOException { logger.info("开始扫描包{}下的所有类", basePackage); return doScan(basePackage, new ArrayList<>()); } /** * Actually perform the scanning procedure. * * @param basePackage * @param nameList A list to contain the result. * @return A list of fully qualified names. * @throws IOException */ private List<String> doScan(String basePackage, List<String> nameList) throws IOException { // replace dots with splashes String splashPath = StringUtil.dotToSplash(basePackage); // get file path URL url = cl.getResource(splashPath); String filePath = StringUtil.getRootPath(url); // Get classes in that package. // If the web server unzips the jar file, then the classes will exist in the form of // normal file in the directory. // If the web server does not unzip the jar file, then classes will exist in jar file. List<String> names = null; // contains the name of the class file. e.g., Apple.class will be stored as "Apple" if (isJarFile(filePath)) { // jar file if (logger.isDebugEnabled()) { logger.debug("{} 是一个JAR包", filePath); } names = readFromJarFile(filePath, splashPath); } else { // directory if (logger.isDebugEnabled()) { logger.debug("{} 是一个目录", filePath); } names = readFromDirectory(filePath); } for (String name : names) { if (isClassFile(name)) { //nameList.add(basePackage + "." + StringUtil.trimExtension(name)); nameList.add(toFullyQualifiedName(name, basePackage)); } else { // this is a directory // check this directory for more classes // do recursive invocation doScan(basePackage + "." + name, nameList); } } if (logger.isDebugEnabled()) { for (String n : nameList) { logger.debug("找到{}", n); } } return nameList; } /** * Convert short class name to fully qualified name. * e.g., String -> java.lang.String */ private String toFullyQualifiedName(String shortName, String basePackage) { StringBuilder sb = new StringBuilder(basePackage); sb.append('.'); sb.append(StringUtil.trimExtension(shortName)); return sb.toString(); } private List<String> readFromJarFile(String jarPath, String splashedPackageName) throws IOException { if (logger.isDebugEnabled()) { logger.debug("从JAR包中读取类: {}", jarPath); } JarInputStream jarIn = new JarInputStream(new FileInputStream(jarPath)); JarEntry entry = jarIn.getNextJarEntry(); List<String> nameList = new ArrayList<>(); while (null != entry) { String name = entry.getName(); if (name.startsWith(splashedPackageName) && isClassFile(name)) { nameList.add(name); } entry = jarIn.getNextJarEntry(); } return nameList; } private List<String> readFromDirectory(String path) { File file = new File(path); String[] names = file.list(); if (null == names) { return null; } return Arrays.asList(names); } private boolean isClassFile(String name) { return name.endsWith(".class"); } private boolean isJarFile(String name) { return name.endsWith(".jar"); } // 获取指定注解修饰的类 public List<String> getClassNameListByAnnotation(List<String> classNames, Class clazz) throws ClassNotFoundException { List<String> annotaionClassNames = new ArrayList<>(); for (String className : classNames) { Class<?> aClass = Class.forName(className); Annotation declaredAnnotation = aClass.getDeclaredAnnotation(clazz); if (declaredAnnotation != null) annotaionClassNames.add(className); } return annotaionClassNames; } /** * For test purpose. */ public static void main(String[] args) throws Exception { ClasspathPackageScanner scan = new ClasspathPackageScanner("co.yiiu"); List<String> classNameList = scan.getFullyQualifiedClassNameList(); System.out.println(classNameList.toString()); } }
文原链接: https://tomoya92.github.io/2019/04/10/mvc-enhancer/
这个类中还用到了一个工具类,代码如下
import java.net.URL; /** * 源码来自:https://blog.csdn.net/neosmith/article/details/43955963 */ public class StringUtil { private StringUtil() { } /** * "file:/home/whf/cn/fh" -> "/home/whf/cn/fh" * "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar" */ public static String getRootPath(URL url) { String fileUrl = url.getFile(); int pos = fileUrl.indexOf('!'); if (-1 == pos) { return fileUrl; } return fileUrl.substring(5, pos); } /** * "cn.fh.lightning" -> "cn/fh/lightning" * * @param name * @return */ public static String dotToSplash(String name) { return name.replaceAll("//.", "/"); } /** * "Apple.class" -> "Apple" */ public static String trimExtension(String name) { int pos = name.indexOf('.'); if (-1 != pos) { return name.substring(0, pos); } return name; } /** * /application/home -> /home * * @param uri * @return */ public static String trimURI(String uri) { String trimmed = uri.substring(1); int splashIndex = trimmed.indexOf('/'); return trimmed.substring(splashIndex); } }
有了扫描包的类了,在启动时直接把项目下的所有类都扫描出来,然后在扫描出来的类中找到被注解修饰的类
找类上注解方法和找方法上注解的方法是一样的,代码如下
其实仔细看看,像不像手写jdbc连接的代码。。。
Class<?> aClass = Class.forName("co.yiiu.xxx"); List<Stirng> annotationClassNames = aClass.getDeclaredAnnotation(Controller.class);
下面来找到被GetMapping注解修饰的方法以及配置的value地址,然后封装到Map里
private Map<String, Map<String, Object>> methodMap = new HashMap<>(); public void init() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { // scan controller ClasspathPackageScanner scan = new ClasspathPackageScanner("co.yiiu"); List<String> classNameList = scan.getFullyQualifiedClassNameList(); List<String> controllerAnnotationClassNames = scan.getClassNameListByAnnotation(classNameList, Controller.class); // 获取GetMapping注解修饰的方法 for (String controllerAnnotationClassName : controllerAnnotationClassNames) { Class<?> aClass = Class.forName(controllerAnnotationClassName); Method[] methods = aClass.getMethods(); for (Method method : methods) { GetMapping declaredAnnotationMethod = method.getDeclaredAnnotation(GetMapping.class); if (declaredAnnotationMethod != null) { // 拿到 @GetMapping("url") 注解里配置的url这个值 String url = declaredAnnotationMethod.value(); Map<String, Object> map = new HashMap<>(); map.put("method", method); // 对应着 index() ... map.put("clazz", aClass.newInstance()); // 对应着 IndexController ... map.put("params", method.getParameters()); // 对应着 index() 的方法参数 ... methodMap.put(url, map); } } } }
我这里用的是freemarker模板作为视图,因为它配置起来相当的简单,基本上就没有配置
唯一的配置就是指定一下模板的加载路径,配置了这一个东东就可以用了,至于缓存以及其它的一些Settings,可以自行配置
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28); // 我把模板放在 resources/templates 里了,所以这里配置了 templates FileTemplateLoader templateLoader = new FileTemplateLoader(new File(ViewResolve.class.getClassLoader().getResource("templates").getPath())); configuration.setTemplateLoader(templateLoader);
完整代码如下
import freemarker.cache.FileTemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import io.undertow.server.HttpServerExchange; import java.io.*; import java.util.Map; /** * Created by tomoya at 2019/4/9 */ public class ViewResolve { private static final Configuration configuration = new Configuration(Configuration.VERSION_2_3_28); static { try { FileTemplateLoader templateLoader = new FileTemplateLoader(new File(ViewResolve.class.getClassLoader().getResource("templates").getPath())); configuration.setTemplateLoader(templateLoader); } catch (IOException e) { e.printStackTrace(); } } // 渲染模板,因为要输出给浏览器,所以这里要用到response // undertow里的response我到现在也不知道怎么获取。。 // 不过它封装了一个 exchange.getResponseSender().send() 方法 // 但freemarker处理模板的时候,要传一个Writer流,这怎么办呢? // 本来是从 exchange 里拿的,因为它有一个方法 exchange.getOutputStream(), 这样我就可以封装成 OutputStreamWriter writer = new OutputStreamWriter(exchange.getOutputStream()); 了 // 但是这样的话,在启动undertow的时候,它会报错,报错内容:java.lang.IllegalStateException: UT000035: Cannot get stream as startBlocking has not been invoked 意思是还没有调用 startBlocking() 方法就想用流,不给用。。。 // 网上找各种解决方案,少的可怜,最后只能看java源码中的Writer接口的实现类,看哪个能用 // 结果还真被我找到了,有个 StringWriter ,果断拿来尝试,结果成功了,可喜可贺可喜可贺 public void render(HttpServerExchange exchange, String templatePath, Map<String, Object> model) { try { Template template = configuration.getTemplate(templatePath + ".ftl"); StringWriter sw = new StringWriter(); template.process(model, sw); exchange.getResponseSender().send(sw.toString()); } catch (IOException | TemplateException e) { e.printStackTrace(); } } }
链文原接: https://tomoya92.github.io/2019/04/10/mvc-enhancer/
到这一步,基本上就算完成了,只需要在undertow接收到请求后,将获取到的地址跟扫描包时整理的Map里地址和方法对应起来执行一下就可以了
原理就是 java.lang.reflect.Method
类里的 invoke()
方法,直接看代码吧
Undertow server = Undertow.builder() .addHttpListener(8080, "localhost") .setHandler(exchange -> { // 获取请求路径 String path = exchange.getRequestPath(); // 从封装好的路径与类,方法信息的Map里通过请求的path来获取定义好的路由方法 Map<String, Object> value = methodMap.get(path); if (value != null) { // 声明一个Map,用于存放controller里执行完要传给模板的内容,类似于springmvc里的ModelAndView对象 Map<String, Object> model = new HashMap<>(); try { // 获取方法所属的类 Object clazz = value.get("clazz"); // 执行方法,第一个参数是这个方法的类实例,后面是可变参数,想传多少传多少,但要跟controller里方法的参数个数,类型一致 Object returnValue = ((Method) value.get("method")).invoke(clazz, exchange, model); // 判断返回值是string,如果是string,则表示它为模板文件名,这样处理目的是为了支持返回json if (returnValue instanceof String) { // 渲染模板视图,响应给浏览器 viewResolve.render(exchange, (String) returnValue, model); } } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } else { // 这里是地址没有找到则响应404 exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); exchange.getResponseHeaders().put(Headers.STATUS, 404); exchange.getResponseSender().send("404"); } }).build(); server.start();
好处
不爽的地方
/user/{id}/add
如果你有思路,请务必告诉我,万分感谢!!
写博客不易,转载请保留原文链接,谢谢!
原文链接: