抄袭Spring和SpringMVC来学习Spring和一些其他的原理

标题叫做抄袭Spring和SpringMVC来学习Spring和一些其他的原理,然而事实上我确实也是这么做的,我就写下这篇日志来记录我的一些思考吧。

引言

我基本上是一个JavaWeb程序员,所以我的侧重点都在JavaWeb的应用上,也就是重点是Servlet。这篇博客主要包括SpringMVC是如何正确工作的,Spring如何管理bean对象的原理(也就是IOC),和Tomcat的一些基本原理等。
我从未经历过写原生Servlet的那个年代,我一毕业开始工作写下第一行代码(那时候我刚从.net、python转向java平台)开始就是直接写Springboot,我也不知道为什么我依着葫芦画瓢画出来的代码就可以用了,我也不知道为什么我打好的jar包war包扔到Tomcat中就能运行了,我也不知道到底这个main函数在哪。所以这篇博客中的一切都是从最基础的部分说起说起。

DI/IOC

我所理解的控制(Inversion of Control)反转和依赖注入(Dependecy Injection)是两码事,不少人比较认同这俩是一个东西,我的理解可能不对,这俩到底啥关系啥区别练习什么的这有点像个学术问题,不讨论也罢,一点也不影响我理解Spring的基本原理和思想。

为什么要控制反转

随着web项目越来越复杂,一般一个简单的web项目包括了如下几个基本组成:

  • Views, Controllers, Models
  • 业务逻辑和服务
  • 持久化与数据库访问

一个请求开始变成了控制层调用业务服务层,服务层调用持久化层这种方式,变成了“三层结构”。此时web服务已经开始变得复杂,业务多变。
由于业务场景的多变,开始引入了面向接口编程的模式,这样可以根据需要设置制定的接口实现类,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface UserDao {
public void save(User user);
}

public class UserDaoRDSImpl implements UserDao {

@Override
public void save(User user) {
System.out.println("我是关系型数据库的持久化实现===insert into user ");
}
}

public class UserDaoNoSQLImpl implements UserDao {

@Override
public void save(User user) {
System.out.println("我是NoSQL的持久化实现===set user ");
}
}
1
2
3
4
5
public static void main( String[] args ) throws Exception {
UserDao DAO = new UserDaoRDSImpl();
// UserDao DAO = new UserDaoNoSQLImpl();
DAO.save(new User());
}

其实不需要多说,就好像Map接口的实现HashMap、LinkedHashMap、TreeMap这些一样,只是为了完成不同的实现。但是日常如果需要经常切换这些接口的实现,那就需要每次切换,重新修改代码,一是麻烦二也带来不必要的风险。由于这些原因有人就提出了控制反转的概念,把这些控制权统统转移到第三方去,不再在Java代码里面控制这些,比如我尝试写一个简单的控制反转,把控制权交到xml中。

我写个很简单的控制反转

我要把控制权放到xml文件中,模仿Spring的BeanFactory来实现,为了能够表现出控制反转和依赖注入的细节,我设置一种不包含依赖注入的方式,看完之后肯定会发现,这种方式的控制几乎屁用没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// bean工厂
public class BeanFactory {

// 把初始化好的bean都丢到这个map中
private Map<String, Object> beanMap = new HashMap<>();

public BeanFactory() throws Exception {
System.out.println("由bean工厂生成对应的bean,并由工厂管理");

// 读取xml文件
SAXBuilder saxBuilder = new SAXBuilder();
Document doc = saxBuilder.build(this.getClass().getClassLoader().getResourceAsStream("beans.xml"));
Element rootElement = doc.getRootElement();
List beanList = rootElement.getChildren("bean");
for (Object bean : beanList) {
Element element=(Element)bean;
String id = element.getAttributeValue("id");
String clazz = element.getAttributeValue("class");
// 实例化并且存储在map中
Object o = Class.forName(clazz).newInstance();
beanMap.put(id, o);
}
System.out.println("装载完成");
}

public Object getBean(String id) {
System.out.println("获取对象" + id);
return beanMap.get(id);
}
}

写xml文件,来指定使用哪个dao实现

1
2
3
<beans>
<bean id="userDao" class="com.haizi.dao.impl.UserDaoRDSImpl" />
</beans>

运行main,根据配置文件把bean进行实例化

1
2
3
4
5
6
7
8
9
public static void main( String[] args ) throws Exception {
System.out.println("读取配置文件...");
BeanFactory applicationContext = new BeanFactory();

UserDao DAO = (UserDao)applicationContext.getBean("userDao");

System.out.println("开始工作");
DAO.save(new User());
}

在main方法中,先用bean工厂加载所有的bean,然后根据name拿到就可以运行了,控制权在xml手里,但是写完这些之后,可以发现,这种最简单的控制反转几乎屁用没有,因为这种控制带来的收益微乎其微,还要新写bean工厂的方法。

实现依赖注入的控制反转

所以只有实现了依赖注入的控制反转才真的有意义,把各个bean依赖的东西也放到xml中去配置,交由bean工厂来管理。
扩展上面的方法,假设service层依赖了dao,需要把实例化好的dao注入到service中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//service需要注入dao才能正常使用save方法
public class UserService {

public UserService() {}

private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void add(User user) {
userDao.save(user);
}
}
1
2
3
4
5
6
<beans>
<bean id="userDao" class="com.haizi.dao.impl.UserDaoImpl" />
<bean id="userService" class="com.haizi.service.UserService" >
<property name="userDao" bean="userDao"/>
</bean>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class BeanFactory {

// 把初始化好的bean都丢到这个map中
private Map<String, Object> beanMap = new HashMap<>();

public BeanFactory() throws Exception {
System.out.println("由bean工厂生成对应的bean,并由工厂管理");

// 读取xml文件
SAXBuilder saxBuilder = new SAXBuilder();
Document doc = saxBuilder.build(this.getClass().getClassLoader().getResourceAsStream("beans.xml"));
Element rootElement = doc.getRootElement();
List beanList = rootElement.getChildren("bean");
for (Object bean : beanList) {
Element element=(Element)bean;
String id = element.getAttributeValue("id");
String clazz = element.getAttributeValue("class");
// 实例化并且存储在map中
Object o = Class.forName(clazz).newInstance();
beanMap.put(id, o);

// 依赖注入
List propertyElementList = element.getChildren("property");
for (Object propertyBean : propertyElementList) {
Element propertyElement = (Element)propertyBean;
String name = propertyElement.getAttributeValue("name");
String beanName = propertyElement.getAttributeValue("bean");
Object beanObject = beanMap.get(beanName);

String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
Method method = o.getClass().getMethod(methodName, beanObject.getClass().getInterfaces()[0]);
method.invoke(o,beanObject);
}
}
System.out.println("装载完成");
}

public Object getBean(String id) {
System.out.println("获取对象" + id);
return beanMap.get(id);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main( String[] args ) throws Exception {

System.out.println("读取配置文件...");
BeanFactory applicationContext = new BeanFactory();

UserService service = (UserService)applicationContext.getBean("userService");
System.out.println("注入成功~");

System.out.println("开始工作");
User u = new User();
u.setUsername("zhangsan");
u.setPassword("zs123456");
service.add(u);
}

用这种方式就实现了依赖注入,其实,我一直感觉“控制反转”这个词真的不好理解,假如当初没有发明这个词,直接叫“依赖注入”,我想理解起来应该会容易非常多,就是把程序所依赖的东西注入进来。
上面的例子描述了service把拿到依赖的dao的过程,那么controller是如何拿到service的呢?答案依然是依赖注入。那么一个web请求是如何找到对应的controller和对应的方法的呢?这一块就是SpringMVC的功能了,但是Spring之前,还要把servlet给弄清楚。

servlet

如何写一个http的api接口

我认真思考了这个问题,我才疏学浅,再加上springMvc先入为主的缘故,我想不出来有什么办法能够做到,我甚至一开始如何用servlet来实现都不知道怎么搞。直到我突然想到三年前的差不多这个时间我写了一套python的接口,给地图提供显示热力图的数据和轨迹数据,那是cgi的方式来实现的接口,url中貌似还有一段“cgi-bin”,搜索了一些相关内容后发现,确实,现在很多脚本语言都是采用的cgi的方式来发布接口。
具体的流程大概为:

  • web服务器接收到请求
  • 根据请求的url找到对应的文件
  • 执行这个(脚本)文件或者某个代码段
  • 把处理结果返回给web服务器
  • web服务器下发处理结果回请求客户端

这样做也是挺好的,但是每次都要执行脚本程序,比如相当于执行” python test.py “ 这种,每次都会起一个新的进程,然后得到返回结果后通过进程间通信发给web服务器。话说路由器中大多配置页面都是cgi的,是C/C++写的。
显然cgi这种方式比较不适合java,原因很简单,java是运行在java虚拟机上的,倘若每次都执行 “ java hello “ ,那将花大量的时间进行初始化java虚拟机,然后再花时间关掉虚拟机,这是非常奇怪的。java要适应动态的web接口和页面,需要一种其他的方式。

他们发明了servlet

所以在1995年,James Gosling提出了servlet这种概念。 想起来一张图

你也配写我的java?

但是由于需要配套一起实现比较复杂的Web服务器,这个服务器要在原Web服务器的基础上加上支持servlet,所以那年也没人重视,所以也没做,加上当时web也才被发明出来三年,js也刚刚被发明,web还主要是静态页面的展示,对动态显示要求不多,所以最终也没有做servlet,也更没有支持servlet的Web服务器了。
随着web应用越来越复杂,要求越来越高,动态显示的东西越来越多,java需要支持这种模式才能保证大家喜欢使用。1997年,SUN公司终于推出Servlet技术,作为java阵营的cgi技术,servlet性能更好,效率更高。
servlet的基本流程大概为:

  • web服务器接收到请求,把请求解析好传递给servlet容器
  • servlet容器根据请求的url找到需要使用的servlet,并且实例化这个servlet
  • 拿到请求,执行servlet实例的逻辑代码
  • 把处理结果返回给servlet容器
  • servlet容器把结果发给web服务器
  • web服务器下发回请求客户端

由此可见,servlet容器和web服务器一样是个常驻进程,随时监听并执行web服务器给的任务分发给各个servlet实例,每次处理请求的时候不在需要开启新的虚拟机开启新的进程,只需要起一个新线程(甚至都不用新建线程)就可以把任务执行了。这种方式显然优异于每次建一个新进程的方式,所以很快就被大众广泛接收。
不过很快大家发现了servlet的不足之处,大量重复的html拼接,为了解决页面显示的问题并且对抗微软推出的asp,1998年servlet又推出了jsp(JavaServet Pages),做了动态页面,可以在jsp页面里写html和java代码,这样servlet主要负责数据和业务逻辑,jsp负责显示,分工协作。
之后的非常长一段时间,都是servlet和jsp在javaweb中作为标杆(其实基本上就是唯一能用的解决方案),直到近些年推崇“前后端分离”,原来的那种jsp开始偏少,也鲜有人在jsp页面里写java了,但是现在javaweb基本上统统都是基于servlet的。(甚至除此之外,我不知道还有什么其他的方法
一方面servlet性能比原先那种cgi的效率高,功能也更加强大,可以方便使用各种池化的资源,方便做数据共享;另外一方面更先进的web服务器带来了servlet容器,允许开发者懒惰,只需要关心业务逻辑,只需要重写对应的模板方法,仿照模板把业务逻辑填充进指定的方法就可以了。

我写一个servlet和它的生命周期

业务开发者开发一个servlet只要根据模板来照着葫芦画瓢:

  • 实现 javax.servlet.Servlet 接口
  • 继承 javax.servlet.GenericServlet,并重写其中的service方法
  • 继承 javax.servlet.http.HttpServlet,并重写其中的doXxx方法中的至少一个

第三种方法肯定是最为推荐的,这是servlet专门整给业务开发者的,GenericServlet是个抽象类,而HttpServlet继承了它,也是一个抽象类,为的是不让开发者能够实例化HttpServlet(不让实例化这是抽象类的一个很重要的功能)。

1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {
void init(ServletConfig config) throws ServletException;

ServletConfig getServletConfig();

void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

String getServletInfo();

void destroy();
}

Servlet通过init方法被实例化,每次被调用都会service方法,被注销调用destroy方法,核心就是这三个方法。假设各个servlet是开启web服务器的时候就init好的,整个流程大概如下:

  • 根据url找到对应servlet
  • 运行这个servlet的service方法
  • 给ServletResponse返回数据

写到这的时候,我已经有点想模仿写个tomcat来看看到底该怎么做了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ThisIsaServlet extends HttpServlet {

@Override
public void init(ServletConfig config) throws ServletException {
//初始化方法
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}


@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理请求
doProcess(req, resp);
}

private void doProcess(HttpServletRequest req, HttpServletResponse resp) {
//相关处理的业务逻辑
resp.getWriter().write("hello hello hehelo!");
resp.getWriter().flush();
resp.getWriter().close();
}
}

HttpServlet的serive已经被写好了,并且根据需要调用了对应的doXxx方法,并且还在注释里写了“There’s no need to override this method.”这句话,意思是:我都已经写好了,你们这些菜鸡不要自己再乱写!并且HttpServlet也已经实现了好几个doXxx方法,但是这些方法的内容都是返回405,意思是给用的人写个模板,这样我们就只需要重写HttpServlet其中的doXxx方法中的至少一个(一个都不写会全报405,没有意义),照着葫芦画瓢,真香。

为什么就跑起来了呢?

我就写了业务逻辑,为什么就能运行了呢?原因在我配置的web.xml中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<web-app>
<servlet>
<servlet-name>testMvc1</servlet-name>
<servlet-class>com.haizi.servlet.Servlet1</servlet-class>
</servlet>
<servlet>
<servlet-name>testMvc2</servlet-name>
<servlet-class>com.haizi.servlet.Servlet2</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>testMvc1</servlet-name>
<url-pattern>/test1</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>testMvc2</servlet-name>
<url-pattern>/test2</url-pattern>
</servlet-mapping>
</web-app>

Tomcat去读取了这个文件,把servlet和servlet-mapping组合起来(通过servlet中的ServletConfig配置这些mapping),servlet-mapping这个交由web服务器管理,servlet交由servlet容器管理,这样来一个web请求的时候,web服务器根据路径找到对应的servlet-mapping,然后再找到对应的servlet,执行它的service方法就行了。所以能跑起来一切都要归功于Tomcat,和Spring 和 SpringMVC没有关系,目前来看一点关系也没有。

SpringMVC

我的同事王平有时候在面试别人的时候会问面试者一个问题:你能列举几个常用的SpringMVC的注解吗?很多人就会回答出一堆其实不相关的东西出来,比如说@Autowired,@Service之类的,其实如果严格划分,这些都不属于SpringMVC,SpringMVC就只是一个MVC框架,能和Spring本身比较好融合而已。同类的东西还有Struts之类的。

什么是MVC和MVC做什么

用java来开发一个web应用有很多种方法: 很久以前,大家把所有逻辑、业务和界面都写在jsp中,这种方式几乎无法维护;后来开始分离JSP和Servlet,出现了Jsp写界面,Servle写数据处理和业务逻辑处理和数据库交互;再后来JSP做页面,Servlet做数据验证,java处理业务逻辑和数据库交互。发展到后面,就变成了三层架构,页面、业务逻辑、数据访问。之后人们就做了MVC这种模式来作为一个的解决方案。(Model-View-Controller和三层架构没有啥关系,MVC和三层结构中对于View的定义一样,但是三层架构没有controller的概念,三层结构是按照业务逻辑来划分,MVC是一种设计模式,而三层架构是一种体系结构。)

servlet和SpringMVC是什么关系

说白了SpringMVC本质上就是一个servlet。
当一个请求到来的时候,tomcat的servlet容器根据请求的url找到需要使用的servlet,然后根据请求方式执行对应的方法,这一切可以在自己写的HttpServlet中完成,也可以使用SpringMVC的DispatcherServlet来实现。
具体怎么实现的我在抄轮子的时候会写。

徒手抄轮子

我本来就想写这个徒手抄袭实现一个Spring和SpringMVC的过程,但是开始写的时候才发现,我不会的太多了,于是边写边学边请教我的朋友王平,最后结果就是:已经写了挺多了,但是我还没开始写Spring和SpringMVC怎么实现。
那就现在开始吧。

Spring管理Bean和他们的生命周期

我们在工程中建的web.xml中,添加一个learner:ContextLoaderListener。配置其获取自己Bean配置信息的配置文件地址,然后就可以把对应的Bean加载到Spring自己的管理器中管理了。我这边做了简化,不再配置Bean配置文件的路径了,直接写了它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ContextLoaderListener implements ServletContextListener {
private WebApplicationContext webApplicationContext;

@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ContextLoaderListener初始化");
webApplicationContext = new WebApplicationContext("applicationContext.xml");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ContextLoaderListener注销");
}
}

public class WebApplicationContext extends AbstractApplicationContext {
/**
* 初始化方法
* @param cfgFile
*/
public WebApplicationContext(String cfgFile) {
super.cfgFile = cfgFile;
// 用来把bean给整到spring的bean工厂里面去
super.refresh();
}
}

AbstractApplicationContext中包含了超多内容:Bean工厂(这其实是一个Map)、要发布的uri和bean的映射关系(这其实也是个Map)、uri和方法的映射关系(这其实也是个Map),还要包含对这些对象的增删查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class AbstractApplicationContext() {
/**
* 配置文件地址
*/
protected String cfgFile = "applicationContext.xml";

/**
* 配置的uri和方法的映射关系
*/
private Map<String, Method> handlerMapping = new ConcurrentHashMap<>();

/**
* 配置的uri和bean的关系
*/
private Map<String, String> controllerMap = new ConcurrentHashMap<>();

/**
* bean工厂
*/
private DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
/** ....
对handlerMapping controllerMap beanFactory 的管理方法
....*/
}

Spring的Bean工厂管理了Spring所需要的所有Bean,所以需要在用这些Bean之前,把这些都初始化完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* bean工厂关键核心部分
*
* 解析xml 扫描包 注册bean 实例化bean url映射
*/
@Override
public void refresh() {
// 解析xml
ApplicationXmlResult result = XmlUtils.readXml(this.cfgFile);
// bean实例化 url和bean
if (null != result) {
List<BeanDefinition> beanDefinitionList = result.getBeanDefinitionList();
// 循环扫描包
this.doScanner(result.getComponentScan(), beanDefinitionList);
// 保存bean
beanFactory.addBeanDefines(beanDefinitionList);
// bean实例化
beanFactory.instanceBeans();
// di 依赖对象注入
beanFactory.injectObject();
} else {
System.out.println("配置文件没读到");
}
}

和文章最开始我写的那个BeanFactory一样,只是从最开始的读取XML文件来生成Bean转成了读取XML文件和扫描扫描包路径下的注解这两种来生成Bean,读取XML的方法和最开始的一样,扫描包的方式核心代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通过递归扫描目录 获取所有bean
*
* @param componentScan 扫描路径
* @param beanDefinitionList
*/
private void doScanner(String componentScan, List<BeanDefinition> beanDefinitionList) {
if (null != componentScan && !"".equals(componentScan)) {
URL url = AbstractApplicationContext.class.getClassLoader().getResource(componentScan.replace(".", "/"));
File dir = new File(url.getFile());
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
doScanner(componentScan + "." + file.getName(), beanDefinitionList);
} else {
String className = componentScan + "." + file.getName().replace(".class", "");
initScanAnnotation(className, beanDefinitionList);
}
}
}
}

用这种方式就可以把自定义的扫描路径下Bean全部都扫描到,扫描后不急着把所有类都进行实例化,只是把类之间的注入关系构建在一起,然后把所有的扫描到的Bean全部置入beanDefinitionList,之后再对所有的Bean进行实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void initScanAnnotation(String className, List<BeanDefinition> beanDefinitionList) {
try {
String beanId = className.substring(className.lastIndexOf("."));
Class<?> clazz = Class.forName(className);
BeanDefinition beanDefinition = new BeanDefinition(beanId, className);
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
//给这个组件注入依赖关系 不是正式的实例化所有bean
PropertyDefinition propertyDefinition = new PropertyDefinition(field.getName(), null, field.getName());
beanDefinition.addPropertyDefinition(propertyDefinition);
}
}

beanDefinitionList.add(beanDefinition);


} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

只注入依赖关系,接下来实例化时候把所有用到的Bean都给实例化了beanFactory.instanceBeans(),然后再用beanFactory.injectObject()把所有依赖关系中所用到Bean全部注入。
这个过程和文章最开始通过XML的方式来管理所有Bean几乎一样,只是加入了注解的方法。

Spring和SpringMVC

我们常用的那些Spring和SpringMVC的注解有:@Autowired @Controller @RequestMapping @RequestParam @ResponseBody @Serive 等,但是我这里侧重实现SpringMVC的基本的简单的原理,所以只实现前四个注解来用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
// 必填
String value();
}

像往常一样写一个Controller

1
2
3
4
5
6
7
8
9
10
11
@Controller
@RequestMapping(value = "hehe")
public class TestController {

@RequestMapping(value = "thisisatest/test")
public String thisisatest(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "test") String test) {
System.out.println("==============>" + test);
return test + "6666666";
}

}

在上面扫描包的时候需要把AbstractApplicationContext中的handlerMapping和controllerMap内容进行填充,我们可以尝试改造之前的initScanAnnotation方法,在扫描包的同时把controllerMap和handlerMapping都给整理好,改造之后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void initScanAnnotation(String className, List<BeanDefinition> beanDefinitionList) {
try {
String beanId = className.substring(className.lastIndexOf("."));
Class<?> clazz = Class.forName(className);
// 整理handlerMapping和controllerMap
if (clazz.isAnnotationPresent(Controller.class)) {
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
String url = annotation.value();
url = baseUrl + "/" + url;
handlerMapping.put(url, method);
controllerMap.put(url, beanId);
System.out.println("接口 " + url + "====>" + beanId + ":" + method);
}
}

BeanDefinition beanDefinition = new BeanDefinition(beanId, className);
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
//给这个组件注入依赖关系 不是正式的实例化所有bean
PropertyDefinition propertyDefinition = new PropertyDefinition(field.getName(), null, field.getName());
beanDefinition.addPropertyDefinition(propertyDefinition);
}
}
beanDefinitionList.add(beanDefinition);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

当一个请求来的时候,这样我可以根据请求的url直接从controllerMap中找到哪个controller,从handlerMapping中找到哪个方法,直接把参数传过去,就可以运行那个方法就可以了。这其实就是SpringMVC所作的内容,根据url找到方法,使用反射的方式运行这个方法,把返回值返回给HTTP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DispatcherServlet extends HttpServlet {
private WebApplicationContext context;

@Override
public void init(ServletConfig config) throws ServletException {
this.context = new WebApplicationContext("applicationContext.xml");
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理请求
doDispatch(req, resp);
}

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
try {
ServletHandler servletHandler = new ServletHandler(this.context);
//servlet分发处理器
servletHandler.handle(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
}

DispatcherServlet这里实现了HTTP请求的接收,初始化ApplicationContext,get和post方法的处理逻辑,最终交由于servletHandler实现业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ServletHandler {

private ApplicationContext context;

public ServletHandler(ApplicationContext context) {
this.context = context;
}

public void handle(HttpServletRequest req, HttpServletResponse resp) throws Exception{
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.substring(1);
if ("".equals(url)) {
resp.getWriter().write("hello hello hehelo!");
resp.getWriter().flush();
resp.getWriter().close();
return;
}

if (context.getHandlerMethod(url) == null) {
resp.getWriter().write("404 NOT FOUND");
resp.getWriter().flush();
resp.getWriter().close();
return;
}

Method method = context.getHandlerMethod(url);
Class<?>[] parameterTypes = method.getParameterTypes();

Map<String, String[]> parameterMap = req.getParameterMap();
Object[] paramValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
String requestParam = parameterTypes[i].getSimpleName();
if ("String".equals(requestParam)) {
for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
String value = Arrays.toString(param.getValue());
paramValues[i] = value;
}
}

}
// 指定对应的业务逻辑方法
Object result = method.invoke(context.getController(url), paramValues);
handleObjectResult(result, resp);

}

// 数据返回
private void handleObjectResult(Object result, HttpServletResponse resp) throws IOException {
resp.setCharacterEncoding("UTF-8");
PrintWriter writer = resp.getWriter();
writer.write(result.toString());
writer.flush();
writer.close();
}
}

我打了一个war包,然后呢

首先遇到的第一个问题是“谁打了一个war包”,是jdk还是tomcat还是Spring,我不知道。下回tomcat的专题中,我再研究研究吧。
我所知道的就只有,最后我在web.xml中配置所有的Url统统走向DispatcherServlet中处理。

1
2
3
4
5
6
7
8
9
10
<web-app>
<servlet>
<servlet-name>testMvc</servlet-name>
<servlet-class>com.haizi.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>testMvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

Tomcat不知道怎么就读取这个web.xml,然后把收到的http请求,统统放到DispatcherServlet中进行处理,处理完返回。至于怎么完成这个动作的,我放到下下篇中再写。

Spring AOP

我累了,下一篇再研究AOP吧。

Tomcat

事情的发展已经超出了我的预期,我必须要抄袭一个tomcat或者自己实现一个tomcat类似的功能才能安心。于是我周末准备弄个tomcat看看能不能搞定。

惶惶恐恐 大半个多月

0%