文章目录
对于一个只会使用SpringBoot而不知道其内部原理的菜鸡,准备系统学习一下Spring的实现。于是第一节课学习的是《仅需2小时 手写MINI Spring MVC框架》。
一、开发准备
1.Gradle
在Web开发时,常用的依赖管理工具通常是Maven。不过Gradle相比于Maven语法更加简单,且不需要安装,更加灵活,所以此项目使用Gradle。
新建项目时特别简单,只需要在idea中使用Gradle创建项目即可。创建完毕项目后,在项目中添加两个模块:framework,test。在build.gradle中,可配置项目或模块的依赖。
子模块可以继承父模块的配置,需要配置在父模块build.gradle中的subprojects项中
2.仿Spring的项目结构
由于Spring有很多功能,所以在我们写的这个mini-spring的framework模块中,以不同的包来划分不同的功能。
Spring的包结构大致如下:
在我们自己的设计中,要实现以下结构:
- Core模块,核心层:Beans、Core、Context。(SpEL是提供表达式的,暂且不实现)
- 实现Web,集成Web和WebMVC
- 添加starter,实现类似于spring-boot的启动方式
于是在framework下创建以下包:
并且在test模块下设置启动类。并且在test中的build.gradle中添加下列配置:
jar{
manifest{
attributes "Main-Class":"com.dlc.Appliation"
}
from{
configurations.compile.collect{
it.isDirectory() ? it : zipTree(it)
}
}
}
其中,manifest即“主清单”,manifest是打出jar包的配置文件,它会被放在META-INF文件夹内。from中是自动打入依赖包的配置。
为了让test模块可以依赖framework模块,还需要在build.gradle的dependencies中添加配置compile(project(':framework'))
。这样在test打出的jar包中就会自动包含framework模块。不过除此之外,还需要在代码层面添加联系。
在framework模块starter包中,添加一个MiniApplication类,作为框架的入口。
public class MiniApplication {
public static void run(Class<?> cls,String[] args){
System.out.println("hello mini-spring");
}
}
在应用(test)的入口类调用这个run方法,是否可以成功运行呢?
public class Application {
public static void main(String[] args) {
System.out.println("hello world");
MiniApplication.run(Application.class,args);
}
}
可以看到成功运行,其实这里的Application也就是Spring项目中的启动类。
二、Web功能开发
1.Web服务器
众所周知,我们的项目想要向外提供服务需要Web服务器。
类似于Tomcat、Nginx、Netty,他们通常都是监听一个TCP端口,然后转发请求、回收响应。它们本身没有逻辑,只是负责连接操作系统和应用程序代码。
2.Web服务模型
我们要实现的就是Web服务器的功能,首先我们要了解,Web服务器是如何连接Web请求和应用程序的。这个过程叫做请求分发。
请求分发其实就像是快递员一样,它负责根据地址转发给不同的应用程序,然后再回收响应返回给请求者。
3.Servlet
Servlet其实是一个广泛的定义,他可以表示一种规范、一个接口,但最常用的定义还是任何实现了javax.servlet.Servlet这个接口的类。
所以Web服务器其实就是接收ServletRequest,经过应用程序处理后,返回ServletResponse的一个功能。
4.集成Tomcat
要自己去实现一个Web服务器,还是挺不现实滴,所以这里选择在项目中集成Tomcat,这是因为Tomcat有以下优点:
- Java原生,运行在JVM上;
- 多种并发模型,高性能;
- 支持嵌入式应用程序
首先在framework的build.gradle添加依赖:compile group: 'org.apache.tomcat.embed',name: 'tomcat-embed-core',version: '8.5.23'
。
然后新建类web.server.TomcatServer:
public class TomcatServer {
private Tomcat tomcat;
private String[] args;
public TomcatServer(String[] args) {
this.args = args;
}
/**
* 启动Tomcat的方法
*/
public void startServer() throws LifecycleException {
tomcat = new Tomcat();
tomcat.setPort(6699);
tomcat.start();
//增加一个常驻线程,防止中途退出
Thread awaitThread = new Thread("tomcat_await_thread"){
@Override
public void run(){
TomcatServer.this.tomcat.getServer().await();
}
};
awaitThread.setDaemon(false); //设为非守护线程
awaitThread.start();
}
}
然后在启动类中使用TomcatServer。(即在MiniApplication中调用startServer方法)这样,在启动项目时,Tomcat便也启动了。
此时项目还是不能运行的,因为Tomcat不知道怎么处理这个Servlet。所以在framework模块的web包下,新建一个servlet包,在其中新建一个实现Servlet接口的TestServlet类。
然后在TomcatServer 中建立Tomcat和Servlet的联系:
Context context = new StandardContext();
context.setPath("");
context.addLifecycleListener(new Tomcat.FixContextListener());
TestServlet servlet = new TestServlet();
Tomcat.addServlet(context,"testServlet",servlet).setAsyncSupported(true);
context.addServletMappingDecoded("/test.json","testServlet");
tomcat.getHost().addChild(context);
此时项目就可以跑起来了,访问/test.json
路径就可以看到写入的值。
三、实现Controller
上面的例子中,直接将Servlet与路径的映射关系写死在框架里,这不用多说,肯定是不对的。而作为MVC中的C——Controller,就可以实现请求分发的功能,那么这是怎么实现的呢?
1.DispatcherServlet
在SpringMVC中,DispatcherServlet就是负责请求转发的Servlet。
DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括一个Handler处理器对象、多个HandlerInterceptor拦截器对象),最后以HandlerExecutionChain对象的形式返回。
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。
Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet;ViewResolver 结合Model和View,来渲染视图,最后将渲染结果返回给客户端。
2.实现Controller注解
将之前TestServlet与Tomcat的对应关系进行修改,将TestServlet改为DispatcherServlet。然后新建一个web.mvc包,在其中就进入了激动人心的实现@Controller
、@RequestMapping
、@RequestParam
注解环节。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}
这是Controller注解的代码,其余两个类似,区别在于其修饰的类型不同,而且@RequestMapping
、@RequestParam
还需要保存值。
然后在test模块写一个controller出来试试:
@Controller
public class SalaryController {
@RequestMapping("getSalary.json")
public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
return 10000;
}
}
是不是真有那味儿了。但是这样还是不行的,因为框架还不知道添加了哪些Controller,所以还要实现类扫描的功能。
3.实现类扫描器
public class ClassScanner {
public static List<Class<?>> scanClass(String packageName) throws IOException, ClassNotFoundException {
List<Class<?>> classList = new ArrayList<>();
String path = packageName.replace(".", "/");
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
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;
}
private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
List<Class<?>> classList = new ArrayList<>();
JarFile jarFile = new JarFile(jarFilePath);
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String entryName = jarEntry.getName(); //com/dlc/test/Test.class
if (entryName.startsWith(path) && entryName.endsWith(".class")) {
String classFullName = entryName.replace("/", ".")
.substring(0, entryName.length() - 6);
classList.add(Class.forName(classFullName));
}
}
return classList;
}
}
4.MappingHandler
需要将Controller中定义的MappingHandler提取出来,使用反射。反射活跃于运行时,可以在运行时动态地获取类的属性和方法,动态实例化类。
定义MappingHandler类,其包括uri
,method
,controller
,args
这几种属性,作为一种映射。
然后定义管理它的HandlerManager类:
public class HandlerManager {
public static List<MappingHandler> mappingHandlerList = new ArrayList<>();
public static void resolveMappingHandler(List<Class<?>> classList){
//一步一步找到类、方法、属性·
for (Class<?> cls:classList){
if(cls.isAnnotationPresent(Controller.class)){
//找到被Controller注解的类
parseHandlerFromController(cls);
}
}
}
private static void parseHandlerFromController(Class<?> cls) {
//使用反射获取这个类的所有方法
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
//找到被RequestMapping注解的方法
if(!method.isAnnotationPresent(RequestMapping.class)){
continue;
}
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 = new MappingHandler(uri,method,cls,params);
HandlerManager.mappingHandlerList.add(mappingHandler);
}
然后,在负责分发的DispatcherServlet使用这个HandlerManager 。
@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();
}
}
}
其中,mappingHandler.handle(req,res)
是判断路径中的uri和MappingHandler中的uri是否相同。
四、Bean管理
上面已经实现了对Controller的解析,不过还要将项目中复杂的逻辑处理到Model里。在Spring中一般以Service表示,当Service过多,多个线程重复创建会造成损耗。所以Spring使用Bean进行管理。
1.Bean
Bean本质上也是对象,只不过比较特殊:
- 生命周期较长,从JVM创建到销毁
- 在整个虚拟机可见
- 维护成本高,单例存在
由于这些特性,Bean的优势如下:
- 运行期效率高,使用时不需要初始化
- 统一维护,便于管理和扩展
就比如在Spring中,我们在Controller里使用Service,只需要@Autowired
注释一下Service即可。但是,如果用new的方式,我们就要先new一个Mapper传给Service,再new一个Service。很麻烦,而且性能差。
在Spring中,Bean的实现方式是:
- 包扫描并自动装配(反射)
- BeanFactory(统一管理)
- 依赖注入
2.控制反转/依赖注入
控制反转(IOC)是一种设计思想,用于解耦。依赖注入(DI)是实现控制反转的一种方式。
假如用普通控制方式,A类中聚合一个X类,则创建A时,要先创建一个X。
当使用控制反转时,除了A和X,还要有一个第三方容器存在,在Spring中就是BeanFactory。在A类通过属性确定它的依赖X之后,BeanFactory就会在创建A之前就先创建好一个X对象,并将X注入A的属性中。
这两者的区别在于,第一种是A主动控制,而第二种A是被动引入。
3.实现依赖注入
- 扫描包获得类定义(之前已完成)
- 初始化Bean,并实现依赖注入
- 解决Bean初始化顺序问题
解决次序问题:
遍历所有的Class,判断是否有依赖,如果没有依赖直接实例化,放到Bean工厂内。
如果其依赖其他的Bean,就要判断其所依赖的是否已经存在于Bean工厂,如果已经存在就用反射set到正在创建的Bean里。如果其所依赖的Bean在工厂中未找到,就先放弃这个Bean,等下一轮遍历再进行判断。
(还要解决多个Bean循环依赖)
4.实现注解
@Bean
和@Autowired
注解与Controller等类似,主要区别修饰的类型。
最重要的是BeanFactory的编写,其控制着Bean的装载。
public class BeanFactory {
//类型与Bean的映射关系
private static Map<Class<?>,Object> clasToBean = new ConcurrentHashMap<>();
public static Object getBean(Class<?> cls){
return clasToBean.get(cls);
}
public static void initBean(List<Class<?>> classList) throws Exception {
List<Class<?>> toCreate = new ArrayList<>(classList);
while(toCreate.size()!=0){
int remainSize = toCreate.size();
for (int i=0;i<toCreate.size();i++){
if(finishCreate(toCreate.get(i))){
toCreate.remove(i);
}
}
if(toCreate.size()==remainSize){
throw new Exception("cycle dependency");
}
}
}
private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)){
//不是Bean就直接不用创建
return true;
}
Object bean = cls.newInstance();
for (Field field : cls.getDeclaredFields()) {
if(field.isAnnotationPresent(Autowired.class)){
//如果某属性需要被装载,则去Bean工厂中查看
Class<?> fieldType = field.getType();
Object reliantBean = getBean(fieldType);
if(reliantBean==null) return false;
field.setAccessible(true);
field.set(bean,reliantBean);
}
}
clasToBean.put(cls,bean);
return true;
}
}
5.测试一下
@Controller
public class SalaryController {
@Autowired
private SalaryService salaryService;
@RequestMapping("/get_salary.json")
public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
return salaryService.calSalary(Integer.valueOf(experience));
}
}
这是test中编写的代码。运行之后的结果:
框架真的奏效了!!!