朋也的博客 » 首页 » 文章
作者:朋也
日期:2019-04-10
类别:java学习笔记
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
上一篇博客介绍了使用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://atjiu.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://atjiu.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://atjiu.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
风格的地址呢?如果你有思路,请务必告诉我,万分感谢!!
写博客不易,转载请保留原文链接,谢谢!