在前面一章节这种,我们已经使用代码实现了仿spring-framework源码手写一个IOC容器并且顺利运行起来了,在这一节中,我们尝试先尝试着实现一个简易的MVC,实现请求跳转;
加耀:仿spring-framework源码实现手写一个IOC容器zhuanlan.zhihu.com在这里,伴随着后面spring源码的学习中,很多地方都是基于注解@Component实现的,那么在这里,我们先来对上一期的IOC容器再进行优化后,再来开始我们的MVC学习之旅吧;
在上一期的ioc容器中,是不支持组合注解的,这里先来将此处进行粗略的优化一下,使其先在功能上支持组合注解;由于在这一节MVC学习中,我们需要使用到SpringMvc中一个比较经典的注解@RequestMapping,这里先来创建一个自定义的RequestMapping注解;
package core.annotation;
import java.lang.annotation.*;
/**
* 访问控制层url处理
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "/";
}
上述代码中,我们创建了一个名为MyRequestMapping的注解,改注解可以在类上使用,也可以在方法上使用,然后它还有一个默认值value为'/';
然后定义一个访问控制层的标志标识注解@MyController
package core.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponent
public @interface MyController {
}
创建好注解后,将上一期的类扫描器ClassPathBeanDefinitionScanner进行一番小改造;由于需要实现组合注解,所以我们需要根据扫描的类,来判断当前类上,是否有我们需要扫描的@MyComponent注解,其代码如下:
/**
* 判断当前类是否包含组合注解 @MyComponent
* interface java.lang.annotation.Documented 等 存在循环,导致内存溢出,所以需要排除java的源注解
* @param classz
*/
private static Class<?> getAnnos(Class<?> classz){
Annotation[] annotations = classz.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() != Deprecated.class &&
annotation.annotationType() != SuppressWarnings.class &&
annotation.annotationType() != Override.class &&
annotation.annotationType() != PostConstruct.class &&
annotation.annotationType() != PreDestroy.class &&
annotation.annotationType() != Resource.class &&
annotation.annotationType() != Resources.class &&
annotation.annotationType() != Generated.class &&
annotation.annotationType() != Target.class &&
annotation.annotationType() != Retention.class &&
annotation.annotationType() != Documented.class &&
annotation.annotationType() != Inherited.class &&
annotation.annotationType() != MyRequestMapping.class
) {
if (annotation.annotationType() == MyComponent.class){
return classz;
}else{
return getAnnos(annotation.annotationType());
}
}
}
return null;
}
在上述代码中,先排除掉所有的java原生注解,然后排除掉刚创建的注解MyRequestMapping;在这里检验当前类是否存在组合注解,然后将当前类的字节码对象进行返回;也就是在这里可能返回的是@MyController、@MyService等注解,后期实现手写Mybatis框架时,还会有一些MyMapping注解等;编写一个测试方法,运行如下:
到了这里,我们只需要将上一期中的ClassPathBeanDefinitionScanner的扫描方法进行一点小小的改动就可以了;如下图所示:
这样一个简单的组合注解的判别就实现了,虽然在spring源码中的组合注解的实现并不是这样来做的;代码已上传到gitlab;
组合注解实现后,我们再来开始我们的简易版的spring-mvc的实现吧;首先,我们创建一个java web工程;点击idea左上角File--> New Module ,选择java,如下图所示:
创建完成后,我们在项目的pom文件中,引入tomcat依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>9.0.22</version>
</dependency>
同时,由于mvc是需要依赖ioc的,所以我们还需要引入我们在上一期中创建的ioc项目,这里我们采用的是多模块的方式,直接在一个项目中引入另外的模块来实现的;另外,也可以通过多个项目间通过本地或者远程私服来进行jar包之间的依赖的;所以这里引入依赖:
<dependency>
<groupId>com.jiayao</groupId>
<artifactId>my-spring-ioc</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
还有就是,由于我们mvc需要获取到访问控制层的方法中的形参信息,在默认情况下通过反射是不能直接拿到参数名的,所以我们还需要使用java8的新特性,这里,我们在pom中指定采用java8插件,这样在反射的时候就可以获取参数的真实名称以及参数类型;
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
当这些都已经准备好了后,我们就可以愉快的开始编写代码了;首先,我们先来定义一个DispatcherServlet类,让它继承HttpServlet类并重写它的 doGet、doPost、init方法,我们先让代码中有这些方法,待会再进一步完善;我们在当前类上添加注解@WebServlet(name = "dispatcherServlet"),标记它是一个WebServlet;
接下来,我们打开项目的webapp路径下的WEB-INF路径下的web.xml文件,内容如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--定义一个Servlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>context.DispatcherServlet</servlet-class>
</servlet>
<!--定义Servlet的请求Url规则-->
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<!--此处先拦截所有-->
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
在上述xml中,我们先配置一个标签<servlet>,配置好我们刚创建的类,然后为它配置一个请求规则,也就是哪些请求会进入这里;
上面配置的DispatcherServlet类中此时有三个方法,分别为doGet、doPost、init;在这个Servlet初始化时,我们应该先把IOC容器给创建出来,所以有了以下代码:
@WebServlet(name = "dispatcherServlet")
public class DispatcherServlet extends HttpServlet {
private AnnotationConfigApplicationContext context;
/**
* 请求url与访问控制层的映射关系
*/
private ConcurrentHashMap<String, HandlerMappers> handlerMappersMap = new ConcurrentHashMap<>();
@Override
public void init() throws ServletException {
/**
* 初始化容器
*/
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.jiayao");
this.context = context;
/**
* 获取所有的Controller类的映射关系
*/
getTargetController();
}
在init()方法中,初始化IOC容器,然后获取所有的候选资源中的携带@MyController的类,获取到访问控制层的类之后,我们就需要获取到访问控制层的请求url与类和方法的映射关系,这样在每个方法请求进来后,只需要根据请求的url就可以直接定位到需要请求哪个Controller的哪个方法了;
private void getTargetController() {
Map<String, BeanDefinition> beanDefinitionMap = context.beanFactory.beanDefinitionMap;
Collection<BeanDefinition> beanDefinitions = beanDefinitionMap.values();
beanDefinitions.forEach(beanDefinitio -> {
if (beanDefinitio.getBeanClass().isAnnotationPresent(MyController.class)) {
getTarHandlerMappers(beanDefinitio);
}
});
}
/**
* 获取访问控制层的映射关系
*
* @param beanDefinitio
*/
private void getTarHandlerMappers(BeanDefinition beanDefinitio) {
String controllerUrl = "";
Class<?> beanClass = beanDefinitio.getBeanClass();
// 获取当前访问控制层全局的url开头
if (beanClass.isAnnotationPresent(MyRequestMapping.class) && StringUtils.isNotEmpty(beanClass.getAnnotation(MyRequestMapping.class).value())) {
String value = beanClass.getAnnotation(MyRequestMapping.class).value();
if (!value.equals("/")) {
if (value.startsWith("/")) {
controllerUrl = value;
} else {
controllerUrl = "/" + value;
}
}
}
String methodUrl = "";
// 获取当前访问控制层所有的方法
Method[] methods = beanClass.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyRequestMapping.class)) {
String value = method.getAnnotation(MyRequestMapping.class).value();
if (StringUtils.isNotEmpty(value)) {
if (!value.startsWith("/")) {
methodUrl = controllerUrl + "/" + value;
} else {
methodUrl = controllerUrl + value;
}
}
if (StringUtils.isNotEmpty(methodUrl)) {
if (handlerMappersMap.containsKey(methodUrl)) {
throw new RuntimeException("当前" + beanClass + "的方法的请求路径与" + handlerMappersMap.get(methodUrl).getMethod() + "配置的路径一致导致冲突了");
}
// 获取当前方法的参数信息
// Parameter[] parameters = method.getParameters();
// for (Parameter parameter : parameters) {
// String name = parameter.getName();
// Type parameterizedType = parameter.getParameterizedType();
// Class<?> type = parameter.getType();
// }
handlerMappersMap.put(methodUrl, new HandlerMappers(methodUrl, context.getBean(beanDefinitio.getBeanName()), method));
} else {
throw new RuntimeException("请为" + beanClass + "的方法" + method + "配置请求url");
}
}
}
}
在上述代码中,根据候选资源中的类,有携带指定注解的,则进行反射获取其具体信息,获取到@MyRequestMapping注解的请求url,然后封装到一个叫做HandlerMappers的类中,也就是保存请求url与请求对象、请求方法的映射关系;
package handler;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.lang.reflect.Method;
@Data
@AllArgsConstructor
public class HandlerMappers {
/**
* 访问的url
*/
private String url;
/**
* 目标对象
*/
private Object targetObj;
/**
* 目标方法
*/
private Method method;
/**
* 存储方法参数
* ....... TODO 预留到下一次添加参数
*/
}
通过上述方法,我们就可以在项目启动后,将所有的请求url的映射关系都保存起来,以便于请求进来后,可以快速找到需要请求的访问控制层;为了方便,我们这边就直接贴出余下代码:
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response);
}
private void processRequest(HttpServletRequest request, HttpServletResponse response) {
// 获取当前请求的url的映射关系
try {
String requestURI = request.getRequestURI();
if (!handlerMappersMap.containsKey(requestURI)) {
response.getWriter().write("404");
}
HandlerMappers handlerMappers = handlerMappersMap.get(requestURI);
/**
* 先做没有参数的情况
*/
Object invoke = handlerMappers.getMethod().invoke(handlerMappers.getTargetObj(), null);
response.getWriter().write(invoke.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
完成以上步骤后,一个超级简单的MVC就完成了;接下来,为当前项目配置一个tomcat即可运行程序;
并且,通过浏览器访问相对应的url,可以执行到相应的访问控制层中;这里先不做方法参数的处理,暂定都是无参的,在下一期中,我们再来进一步完善这个mvc代码吧;
代码地址:https://gitlab.com/qingsongxi/spring-mvc.git