综述
本文主要参考了昵称为“枕边书”的大佬手写的一个mini springmvc视频。我的代码放在了这里。在本文中我将尽可能对于源代码进行比较细致的讲解,当然个人能力有限,有些错误在所难免,希望大家批评指正。
目录结构
整个工程整体的代码结构如下,整个工程包含了两个部分:
mvc-framework
是本文所要讲的springmvc这个简易框架,其中记录了这个简易的springmvc是如何工作的。而my-application
则是个人应用程序,它将mvc-framework包导入。通俗理解一下就是,你可以把mvc-framework
理解成我们平时使用的SpringMVC的jar包,然后my-application
就是基于SpringMVC自己开发的java web应用,而我们在应用开发的时候需要导入SpringMVC的jar包。
1 framework代码详解
在讲解之前我们首先看一下mvc-framework
模块下的每个包中都放了些什么东西。
annotation
中是模拟的一些SpringMVC中的注解,当然这些注解的功能没有SpringMVC中的那么全面。由于是模拟了相应的注解,所以在我的代码中我都以'My'开头。beans
下的BeanFactory类用于管理bean,例如,一个@MyController注解的实例,就是一个bean。core
中的ClassScanner主要用于执行组件的扫描。handler
用于管理url映射和对应的Controller或者method间的对应关系。server
中放了一个TomcatServer,我们这里是采用了内嵌tomcat的形式(这也让我想到了springboot中也内嵌了tomcat)。servlet
中的MyDispatcherServlet就是模拟了DispatcherServlet。另外我们还为当前框架设置了一个启动入口类放在starter
包下。
1.1 注解
我们先来看一下注解部分,@MyAutoWired
注解的代码如下,关于注解的相关参数,大家可以去网上找一些资料查看一下,在这里这个注解添加在字段上。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAutoWired {
}
其他的几个注解我也一并放在这里。注解这一块比较简单,大家可以直接看源码,如何对添加了注解的实例进行扫描和解析,后文会讲。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyBean {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyController {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //为了简单,此注解只能添加在 方法上,且只支持一个uri
public @interface MyRequestMapping {
String value(); //value可以指定映射路径
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface MyRequestParam {
String value(); //value可以指定请求参数中需要的参数名
}
1.2 启动入口类MVCApplication
说道这个,可能有些时候不能反应过来这是个啥,但是如果你有过一些SpringBoot的开发经验,那你一定接触过下面的应用
启动类:
@SpringBootApplication
@ComponentScan(basePackages={"aaa.bbb.ccc.*"})
@MapperScan("111.222.333.mapper")
public class XxxYyyApplication {
public static void main(String[] args) {
SpringApplication.run(XxxYyyApplication.class, args);
}
}
MVCApplication
就相当于上面代码中的SpringApplication
,实际上你点开MVCApplication
的源码,你会发现它也恰好有一个 run() 方法(hhh确实是有点模拟springboot的味道),注意这里传入的class参数为用户自己的应用程序启动类的类型。
public class MVCApplication {
public static void run(Class<?> cls) {
System.out.println("=============================start!!!!==========================");
TomcatServer tomcatServer = new TomcatServer(); //1
try {
tomcatServer.startServer();
List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName()); //2
BeanFactory.initBean(classList); //3
MyMappingHandlerManager.resolveMappingHander(classList); //4
for (MyMappingHandler myMappingHandler : MyMappingHandlerManager.mappingHandlerList) {
//5
System.out.println(myMappingHandler.getUri() + ":" + myMappingHandler.getMethod());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
run 方法整体的思路比较简单:
- 1 生成内嵌的Tomcat服务器后启动它
- 2 以传入的用户应用程序启动类所在的包为基础,进行组件扫描
- 3 将扫描的类,进行bean实例化,交由BeanFactory管理
- 4 解析url映射,完成这一步就让系统知道出现某个url的请求,应该对应到哪个controller的哪个方法上去。这一部分后文会详解
- 5 打印映射路径和其对应的方法(可忽略)
1.3 内嵌的Tomcat的配置和启动
内嵌tamcat的使用和配置可以参见实现内嵌tomcat,这里不再赘述了。
public class TomcatServer {
private Tomcat tomcat;
public TomcatServer() {
}
public void startServer() throws LifecycleException {
this.tomcat = new Tomcat();
tomcat.setHostname("localhost");
tomcat.setPort(8080);
Context context = new StandardContext();
context.setPath("");
context.addLifecycleListener(new Tomcat.FixContextListener());
MyDispatcherServlet dispatcherServlet = new MyDispatcherServlet();
Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet).setAsyncSupported(true);
context.addServletMappingDecoded("/", "dispatcherServlet");
tomcat.getHost().addChild(context);
tomcat.start();
Thread awaitThread = new Thread("tomcat_await_thread"){
@Override
public void run(){
TomcatServer.this.tomcat.getServer().await();
}
};
awaitThread.setDaemon(false);
awaitThread.start();
}
}
startServer
方法就做了两件事,一是配置,二是启动tomcat。配置了主机名,端口号,上下文路径为空(意味着8080/后面可以直接跟着请求路径了),并且添加注册了一个servlet,名为dispatcherServlet,具体是MyDispatcherServlet
实例(马上就讲)。
1.4 MyDispatcherServlet
有过java web开发经验的朋友肯定对DispatcherServlet不陌生。老样子,直接上代码。
public class MyDispatcherServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
for (MyMappingHandler mappingHandler : MyMappingHandlerManager.mappingHandlerList) {
if(requestURI.equals(mappingHandler.getUri())){
try {
mappingHandler.handle(req, resp);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
break;
}
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
为了简单,我们这里不去区分get和post。doGet方法也很清晰,首先要获得请求的请求路径,然后从所有的映射关系封装类型MyMappingHandler
的列表中,找到对应该请求的那个MyMappingHandler
对象,并由它来处理这个请求。现在也许还不太清楚MyMappingHandler
到底是啥(因为我还没讲hhhh),要知道它存放了映射路径的uri和对应的method,以及可以处理相应的请求就ok啦。这里也可以看出DispatcherServlet只是负责的请求的分发,并不处理请求。
1.5 类扫描ClassScanner
至此我们算是进入到了比较核心的几个部分。首先就是上文提到的类扫描,主要逻辑在ClassScanner.scanClasses()
方法中(如果忘了springmvc是如何启动的,请返回上文看1.2 节)。首先这里我们要明确,所谓的类的扫描,扫描的是哪些类,是framework中的类?还是我们自己写的应用程序中的类?很显然是后者。我们先贴代码。
public static List<Class<?>> scanClasses(String basePackageName) throws IOException, ClassNotFoundException {
List<Class<?>> classList = new ArrayList<>();
//这里的packageName是 用户应用程序入口类所在的package名称,以它为基础进行组件扫描
String path = basePackageName.replace(".", "/");
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
String protocol = resource.getProtocol();
if (protocol.contains("jar")) {
//如果应用程序是以jar包的方式运行(java -jar),那么就会进入到这里面。
JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
String jarFilePath = jarURLConnection.getJarFile().getName();
classList.addAll(getClassesFromJar(jarFilePath, path));
} else if (protocol.contains("file")) {
//有时候我们想要对程序进行调试,而不是以jar方式运行。那么就会进入到这里。
String fileOrPath = resource.getFile();
classList.addAll(getClassFromPath(fileOrPath, basePackageName));
}
}
return classList;
}
这里有两种情况,一种是用户的应用程序会打成一个jar包,然后java -jar运行这个jar包,那么scanClasses就需要在jar包中进行类的扫描(后文测试的部分我会演示运行jar包);另一种是有些时候我们想要调试程序,那么编译之后的类会放在target目录中,需要在target中进行类的扫描。关于jar的方式如何进行扫描的,我也没有仔细研究过。不管怎么样,组件扫描的最终结果就是,将应用程序中用户的类都扫描到放入classList中返回,所以这部分就不展开讲了。
//扫描jar包中的类
private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
JarFile jarFile = new JarFile(jarFilePath);
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String entryName = jarEntry.getName();
if (entryName.startsWith(path) && entryName.endsWith(".class")) {
String classFullName = entryName.replace("/", ".").substring(0, entryName.length() - 6);
classes.add(Class.forName(classFullName));
}
}
return classes;
}
//扫描target目录下的类
private static List<Class<?>> getClassFromPath(String fileOrPath, String packageName) throws ClassNotFoundException {
List<Class<?>> list = new ArrayList<>();
List<String> listOfClassAbsoluteName = new ArrayList<>();
File[] files = new File(fileOrPath).listFiles();
recursiveFindClass(listOfClassAbsoluteName, files);
int l = fileOrPath.length();
for (String classAbsoluteName : listOfClassAbsoluteName) {
String className = packageName + "." + classAbsoluteName.substring(l, classAbsoluteName.length() - 6).replace(String.valueOf(File.separatorChar), ".");
list.add(Class.forName(className));
}
return list;
}
private static void recursiveFindClass(List<String> list, File[] files) {
if (files == null) {
return;
}
for (File file : files) {
if (file.isFile() && file.getName().endsWith(".class")) {
list.add(file.getAbsolutePath());
} else if (file.isDirectory()) {
recursiveFindClass(list, file.listFiles());
}
}
}
好了第(一)部分就先写到这里了,辛苦大家观看到这里,(二)部分主要讲解bean的实例化以及MappingHandler等,(三)部分就是一个十分简单的应用。谢谢大家!
甩一下连接:
·手写简易SpringMVC框架(二):bean实例化、MappingHandler
·手写简易SpringMVC框架(三):极其简单的应用