为了对SpringMVC有个更加深入的理解,通过自定义一个类SpringMVC框架来模拟框架中的核心模块和实现。
本项目采用Gradle版本控制工具进行构建(相比Maven更加方便,而且免安装)
在书写之前,先来了解一下Spring框架的包结构。简要来说可以分为两层:
(1)第一层:Core核心层,这一层中主要的功能包括下面几个:
(a)Beans :负责Spring中对Bean对象的管理
(b)Context : Spring根据不同场景提取出来的框架接口,使得调用者可以直接接触Spring内部
(c) SpEL:Spring表达式的语言包
(2)第二层:应用层,这一层主要是使用核心层的接口为应用程序提供特定的功能
可以分为两大类:
(a)Data :用于数据的接入与处理等操作 包括JDBC,ORM等
(b)Web应用: 包括MVC,Servlet
好了,了解了Spring框架内部功能之后,开始这个项目的结构设计。
由于有些功能较为复杂,本项目主要实现为:
(1)实现Core模块,包括Core,Beans,Context包
(2)实现Web,集成Web和Web MVC
(3)添加starter,实现类SpringBoot的启动方式
开始书写项目:
在IDE中新建一个Gradle项目,建一个框架模块(framework)和一个测试模块(test)。
额外在test模块建立一个Application启动类(写一个main函数)。
在命令行运用Gradle clean build和 java - jar 文件目录的方式进行打包,此时会如下报错:
adeMacBook-Pro:SpringMVC mac$ java -jar test/build/libs/test-1.0-SNAPSHOT.jar
test/build/libs/test-1.0-SNAPSHOT.jar中没有主清单属性
原因在于虽然在test中新建了启动类,但是Gradle打出的包并不知道它在哪儿,所以需要人为显示地声明它,(Gradle中所有的jar包操作都要定义在jar关键字里),同时也利用from递归地打入所有依赖的jar包(it代笔当前文件,如果是只一个独立文件的话则直接打入;否则还有其他依赖,递归打入)
test模块中的build.gradle配置如下:
plugins {
id 'java'
}
group 'ydq.mooc.com'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile(project(':framework'))
}
jar{
manifest{
attributes "Main-Class":"com.mooc.ydq.Application"
}
from{
configurations.compile.collect{
it.isDirectory() ? it : zipTree(it)
}
}
}
配置之后,重新打包,得到正确的结果:
adeMacBook-Pro:SpringMVC mac$ java -jar test/build/libs/test-1.0-SNAPSHOT.jar
Hello World!
接下来需要把framework包打入到test模块中:
(1)在test模块的build.gradle文件依赖中添加:
compile(project(':framework'))
(2)接着在framework模块中新建starter包,下面定义这个模块的启动类MiniAppplication,并实现一个run方法(参数为一个类和一个参数数组)
public static void run(Class<?> cls, String[] args)
(3)在test模块的启动类中调用:
public class Application {
public static void main(String[] args) {
System.out.println("Hello World!");
//第一个参数为当前入口类,第二个为当前入口类的参数数组
MiniApplication.run(Application.class, args);
}
}
然后打包,得到结果:
adeMacBook-Pro:SpringMVC mac$ java -jar test/build/libs/test-1.0-SNAPSHOT.jar
Hello World!
Hello min-spring!
(mini-spring为miniApplication run方法中的输出)
此时就说明这两个模块已经成功建立了联系。
于是乎,开始开发Web服务模型和Servlet。在开发之前,需要明确Web服务器的请求分发机制:
Web服务器通过监听一个TCP端口,然后将客户端的请求进行转发,经过Servlet的处理之后,最后服务器再将处理结果返回给客户端。
需要特别注意的是:Web服务器本身不会进行业务逻辑的处理,而是起到连接操作系统和应用程序的作用。
Web服务器的请求转发流程如下:
而Servlet呢,就是用于实现Web服务器分发过来的业务逻辑。通常来说,一个Servlet用来实现一种特定的业务逻辑,对应一种uri。
在项目中,选择Tomcat服务器,并将服务器集成到Spring框架中:
具体做法为:先在依赖中引入Tomcat-core的依赖,调用Tomcat包,实例化一个Tomcat类并调用其start方法即可。
先上Maven中心仓库
org.apache.tomcat.embed " tomcat-embed-coremvnrepository.com选择Tomcat-core的8.5.23版本,复制gradle的配置,然后在框架中将其内嵌进去:
(注意引入依赖的位置一定要在framework的build.grandle文件中!!)
plugins {
id 'java'
}
group 'ydq.mooc.com'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.5.23'
}
然后实例化Tomcat,新建一个TomcatServer类:设置Tomcat端口号为6699
(代码中为了防止Tomcat服务器中途挂掉,采用了另外一个等待线程)
//专门用于处理tomcat server的类
public class TomcatServer {
private Tomcat tomcat;
//用于tomcat的配置参数
private String[] args;
//tomcat的constructor方法
public TomcatServer(String[] args){
this.args = args;
}
//启动tomcat的主方法
public void startServer() throws LifecycleException {
//实例化一个tomcat即可
tomcat = new Tomcat();
//设置tomcat的监听方法
tomcat.setPort(6699);
tomcat.start();
//为了防止服务器中途退出,设置一个等待线程
Thread awaitThread = new Thread("tomcat_await_thread"){
@Override
public void run(){
//声明tomcat线程一直在等待
TomcatServer.this.tomcat.getServer().await();
}
};
//将此线程设置为非守护线程
awaitThread.setDaemon(false);
//调用方法,让它一直在等待
awaitThread.start();
}
}
书写完TomcatServer类之后呢,在MiniApplication中启动Tomcat:
public class MiniApplication {
public static void run(Class<?> cls, String[] args){
System.out.println("Hello min-spring!");
TomcatServer tomcatServer = new TomcatServer(args);
try {
tomcatServer.startServer();
} catch (LifecycleException e) {
//打印异常栈
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
然后将项目打包运行,可以在控制台看到Tomcat服务已经启动成功,但是用浏览器访问会报404,这里是为什么呢?
原因在于Tomcat中没有注册Servlet,所以继而定义一个Servlet类(继承Servlet父类),重写里面的service方法
//需要实现五个预定义的方法-->本项目只需要实现service方法即可
public class DispatcherServlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
//在响应内写入test即可
res.getWriter().println("test");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
然后在Tomcat中注册该Servlet:
在书写之前呢,要了解Tomcat内部的四种容器类型:
(1)Container 容器,集装箱
(2)Engine 引擎容器
(3)Host 主机容器
(4) Servlet容器(Tomcat中级别最低的容器)里面包含了一个或多个Context
Context(上下文,背景,环境)一个Context对应于一个Web项目
一个Context中包含若干个Wrapper(容器里的封装部分)
Tomcat通过这四种类型对容器的职责进行解耦。其中和Servlet最接近的就是Context容器。
注册的代码如下:
//启动tomcat的主方法
public void startServer() throws LifecycleException {
//实例化一个tomcat即可
tomcat = new Tomcat();
//设置tomcat的监听方法
tomcat.setPort(6699);
tomcat.start();
//StandardContext为tomcat内对于context容器的标准实现
Context context = new StandardContext();
context.setPath("");
//设置一个默认的监听器
context.addLifecycleListener(new Tomcat.FixContextListener());
//将test实例化注册到servlet容器中
DispatcherServlet servlet = new DispatcherServlet();
Tomcat.addServlet(context, "dispatcherServlet", servlet).setAsyncSupported(true);
//配置一个servlet到uri的映射 (这样使得访问这个uri时可以映射到这个servlet)
context.addServletMappingDecoded("/", "dispatcherServlet");
//Tomcat的context容器需要依附在host容器内,这里将其注册到默认的host容器
tomcat.getHost().addChild(context);
//为了防止服务器中途退出,设置一个等待线程
Thread awaitThread = new Thread("tomcat_await_thread"){
@Override
public void run(){
//声明tomcat线程一直在等待
TomcatServer.this.tomcat.getServer().await();
}
};
//将此线程设置为非守护线程
awaitThread.setDaemon(false);
//调用方法,让它一直在等待
awaitThread.start();
}
打包运行一下,浏览器中会输入地址会返回 “test”,验证成功。
但是这样做会存在很大的弊端,因为如果容器中的Servlet过多,那么每定义一个新的Servlet就得在Tomcat中注入一个,在操作上显得非常繁琐,而且使得Servlet的维护成本大大升高。
因此需要进行优化,优化的点在于Tomcat要能够动态地实现Servlet与uri的映射关系。
一种做法是在web.xml中进行中心话配置,每种业务增加一个servlet,就对应增加一个URI与之对应,调度流程如下图所示:
但这样做呢,缺点也很明显:
(1)web.xml会使得Servlet的配置集中化,大而3杂,并且不易管理
(2)每次手动注入之前,都要新定义Servelt接口的实现
所以,就引出了SpringMVC框架中采用的调度模型,如下:
且听我慢慢解释:
这个模型呢,是利用一个叫做DispatcherServlet的类来进行所有Servlet的分发,对应哪一个Serlvet就去容器中的HandlerMapping去找即可,这种调度将外部不同的请求都接到一个类上(DispatcherServlet类),再进行一对多的分发。
这种调度的最大优点在于配Servlet的时候只需要在框架中进行修改即可,而不需要再Tomcat中堆Context进行操作了。
在原有的项目中Servlet的映射路径改为"/" :
//配置一个servlet到uri的映射 (这样使得访问这个uri时可以映射到这个servlet)
context.addServletMappingDecoded("/", "dispatcherServlet");
真正请求时再加载根路径下面的子路径即可。
然后新建一个MVC包,用于请求的分发,而每个Serlvet怎么区分呢?
这里就可以类比SpringMVC中控制器常用的三个注解:Controller, RequestMapping和RequestParam。
新建三个类来分别实现这三个注解(注意:注解的本质其实就是接口)
(1)controller注解(作用在类上):
@Documented
//需要保留到运行期
@Retention(RetentionPolicy.RUNTIME)
//说明此注解是类的注解
@Target(ElementType.TYPE)
public @interface Controller {
}
(2)RequestMapping注解(作用在方法上):
@Documented
@Retention(RetentionPolicy.RUNTIME)
//说明此注解是方法的注解
@Target(ElementType.METHOD)
public @interface RequestMapping {
//添加一个属性,保存需要映射的uri
String value();
}
(3)RequestParam注解(注解在参数上):
@Documented
@Retention(RetentionPolicy.RUNTIME)
//此注解用于接收参数
@Target(ElementType.PARAMETER)
public @interface RequestParam {
String value();
}
完成注解之后,在test包下面测试一下这一个注解:
写随意书写一个Controller:
@Controller
public class SalaryController {
@AutoWired
private SalaryService salaryService;
//功能:计算工资(通过员工姓名和工龄来计算)
@RequestMapping("/get_salary.json")
public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
return 1000;
}
}
打包之后,发现在浏览器中访问不到,这又是什么情况呢?
原因是服务器根本就不知道这个Controller,得想办法让服务器扫描到这个Controller吧。
这里就采用的Java中的类加载器,简单介绍一下:
类加载器是通过 类全限定名 来获取类的二进制字节流,再将二进制字节流进行解析,获取Class类的实例,可以加载classpath下的静态资源。
Java所有的类文件都可以看成是Resource的抽象,每个Java类文件都是与类名相对应的,包名和文件夹路径相对应。
好了,了解之后,开始书写:
在framework模块中新建一个core包,添加一个类扫描器ClassScanner:
//功能:类扫描器
public class ClassScanner {
//传入的参数是包名---> 进行类的加载
public static List<Class<?>> scanClasses(String packageName) throws IOException, ClassNotFoundException {
List<Class<?>> classList = new ArrayList<>();
//将包名转化为文件路径
String path = packageName.replace(".", "/");
//使用类加载器--->通过路径来加载文件
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//遍历类加载器的url方法,返回值是一个可遍历的url资源
Enumeration<URL> resources = classLoader.getResources(path);
while(resources.hasMoreElements()){
URL resource = resources.nextElement();
//如果resource的类型是jar包类型,则获取jar包的绝对路径
if(resource.getProtocol().contains("jar")){
//需要强转为jar型
JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
String jarFilePath = jarURLConnection.getJarFile().getName();
classList.addAll(getClassesFromJar(jarFilePath, path));
}else{
//todo
}
}
return classList;
}
//功能:通过jar包的路径来获取到jar包下全部的类
private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
//初始化容器来存储类
List<Class<?>> classes = new ArrayList<>();
//将jar包路径转化为jarFile实例
JarFile jarFile = new JarFile(jarFilePath);
//进行遍历
Enumeration<JarEntry> jarEntries = jarFile.entries();
while(jarEntries.hasMoreElements()){
JarEntry jarEntry = jarEntries.nextElement();
String entryName = jarEntry.getName();//例如com/mooc/ydq/test/Test.class
//目标是取出路径的开头与我们传入的path参数相同的jar文件,并且结尾一定是class,这样即可拿到对应的jar包了
if(entryName.startsWith(path) && entryName.endsWith(".class")){
//将/换成. 将.class后缀去掉
String classFullName = entryName.replace("/", ".").substring(0, entryName.length() - 6);
//通过class.forName方法将类加载到jvm中
classes.add(Class.forName(classFullName));
}
}
return classes;
}
}
在MiniApplication启动类中打印所有的类:
List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());
classList.forEach(it-> System.out.println(it.getName()));
然后在进行打包测试:
可以看到,打印出了包下面所有的类。
写完Controller之后并不能直接利用,而应该将其中的MappingHandler提取出来。
这里采用Java中的反射机制来实现,使用反射,我们可以保存Controller中的MappingHandler,当有请求到来时进行调用。(每个MappingHandler都是一个请求映射器,对应于特定的URI)
书写一个请求映射器MappingHandler:
//功能:请求映射器
public class MappingHandler {
//它对应的请求uri
private String uri;
//它对应的controller方法
private Method method;
//controller的类
private Class<?> controller;
private String[] args;
MappingHandler(String uri, Method method, Class<?> cls, String[] args){
this.uri = uri;
this.method = method;
this.controller = cls;
this.args = args;
}
}
然后再写一个HandlerManager类来管理若干Handler:
public class HandlerManager {
//设置一个静态属性,用来保存多个mappingHandler
public static List<MappingHandler> mappingHandlerList = new ArrayList<>();
//把controller类挑选出来,将类中的mappingHandler初始化为mappinghandler
public static void resolveMappingHandler(List<Class<?>> classList){
//首先遍历类,将带有@controller注解的挑选出来
for (Class<?> cls : classList){
//如果这个类上有@Controller注解
if(cls.isAnnotationPresent(Controller.class)){
parseHandlerFromController(cls);
}
}
}
其中方法的实现如下:
private static void parseHandlerFromController(Class<?> cls){
//通过反射的方式获取类中的所有方法
Method[] methods = cls.getDeclaredMethods();
//然后找到被@RequestMapping的方法
for (Method method : methods){
if(!method.isAnnotationPresent(RequestMapping.class)){
continue;
}
//从方法的属性中获取所有能构成MappingHandler的属性
//uri
String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
//设置一个容器来存储参数
List<String> paramNameList = new ArrayList<>();
for (Parameter parameter : method.getParameters()){
//找到被@RequestParam注解的参数
if (parameter.isAnnotationPresent(RequestParam.class)){
paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
}
}
String[] params = paramNameList.toArray(new String[paramNameList.size()]);
//然后构造一个mappingHandler,将参数传入进去
MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);
//最后把构造好的mappingHandler放到handler管理器的静态属性中
HandlerManager.mappingHandlerList.add(mappingHandler);
}
}
}
写好之后呢,就可以在DispatcherServlet中使用Handler了,做法如下:
修改其中的Service方法:
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList){
try {
if (mappingHandler.handle(req, res)){
return;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
handle方法去MappingHandler类中实现:
public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
//将serlvetRequest转化为HttpServletRequest
String requesturi = ((HttpServletRequest) req).getRequestURI();
//判断uri和handler中的uri是否相等
if(!uri.equals(requesturi)){
return false;
}
//如果相等,则调用method的方法
Object[] parameters = new Object[args.length];
//通过参数名依次从servletRequest中获取这些参数
for (int i = 0; i < args.length; i++){
parameters[i] = req.getParameter(args[i]);
}
Object ctl = BeanFactory.getBean(controller);
//因为controller可能会是多种类型,所以用Object类来存储结果
//Object ctl = controller.newInstance();
Object response = method.invoke(ctl, parameters);
//将方法返回的结果放到servletResponse中去
res.getWriter().println(response.toString());
return true;
}
最后,不要忘记在框架入口类MiniApplication中调用HandlerManager,初始化所有的MappingHandler。
HandlerManager.resolveMappingHandler(classList);
进行打包测试,浏览器访问后即可请求到“1000”,成功。
以上就完成了Context,starter以及WebMVC等功能,下一篇文章会接着实现core模块中的Bean对象部分,敬请期待。