申明:仅作为个人学习笔记,原文请访问: 手写Spring MVC
如果需要源码,请点击上面的链接。
总体思路:
1、Spring MVC是通过注册DispacherServlet,并配置url-pattern为/* 来接管所有的请求。同样的,如果我们需要实现Mvc框架,也需要在web.xml中注册一个自定义的servlet:
<servlet>
<servlet-name>xmvc</servlet-name>
<servlet-class>com.ph.xspring.servlet.XDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--you can't use classpath*: -->
<param-value>application.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>xmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
2、实现MVC的功能,还需要配置一些常用的注解,例如@RequestMapping, @Controller等等。下面列举一个自定义的注解
@Target({ElementType.TYPE}) //限定注解的使用位置
@Retention(RetentionPolicy.RUNTIME) //注解的保留政策,如果想要通过反射来获取注解信息,需要在内存中保留注解信息,只能用runtime
@Documented //用于制作文档
public @interface XController {
String value()default "";
}
3、接着要实现XDispacherServlet中的内容,在这个Servlet内读取配置文件、初始化参数、IOC容器初始化、依赖注入等等。这些列初始化的功能,都需要重写HttpServlet中的init方法,在init方法中实现。
4、初始化完成后,就需要接收并处理浏览器传过来的请求了,将service(GET,POST为例)方法中的请求交给doDispacher来处理,并返回结果。
下面来解析XDispacherServlet中的内容:
1、静态变量
public class XDispatcherServlet extends HttpServlet {
/*
属性配置文件
*/
private Properties contextConfig = new Properties();
private List<String> classNamelist = new ArrayList<>();
/**
* IOC容器
*/
Map<String ,Object>iocMap = new HashMap<String,Object>();
Map<String, Method>handlerMapping = new HashMap<String, Method>();
首先是上面几个变量,在下面的init方法中,需要对上述变量进行初始化。
contextConfig用于表示配置文件,例如applicationContext
classNameList用来存放用户指定包下的所有java类的字节码的名称,用于后面通过反射来获取类信息。
iocMap用来表示IOC容器,通过注解或者xml文件注入进来的对象(本文仅通过注解注入),会存放在此。
handlerMapping用来存放url与对应方法之间的映射关系,例如 在controller中 ”/login“ 路径与 Login()方法之间是对应的,通过访问/login,即可调用Login()方法。
2、初始化
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scan-package"));
//3.初始化IOC容器,将所有的相关类实例保存到IOC容器中
doInstance();
//4.依赖注入
doAutowired();
//5.初始化HandlerMapping
initHandlerMapping();
初始化有上述五个步骤,下面会一 一说明。
2.1、加载配置文件
private void doLoadConfig(String contextConfigLocation) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
//保存在内存
contextConfig.load(inputStream);
System.out.println("[INFO-1] property file has been saved in contextConfig.");
} catch (IOException e) {
e.printStackTrace();
}finally {
if(null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
需要传入文件名,该文件放在resources目录下,我在该文件配置了所需要扫描的包(下面一个步骤用到),如下
scan-package=com.ph
2.2、扫描相关类
private void doScanner(String scanPackage) {
URL resourcePath = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\.","/"));
System.out.println("INFO-2 "+resourcePath);
if(resourcePath == null){
return;
}
File classPath = new File(resourcePath.getFile());
for(File file: classPath.listFiles()){
if(file.isDirectory()){
System.out.println("[INFO-2] {" + file.getName() + "} is a directory.");
//子目录递归
doScanner(scanPackage + "." + file.getName());
}else{
if(!file.getName().endsWith(".class")){
System.out.println("[INFO-2] {" + file.getName() + "} is not a class file.");
continue;
}
String className = (scanPackage+"." +file.getName()).replace(".class","");
classNamelist.add(className);
System.out.println("[INFO-2] {" + className + "} has been saved in classNameList.");
}
}
}
将指定包下所有字节码文件,添加到classNameList中。
2.3、初始化IOC容器
private void doInstance() {
if(classNamelist.isEmpty()){
return;
}
try {
for(String className: classNamelist){
Class<?>clazz = Class.forName(className);
if(clazz.isAnnotationPresent(XController.class)){
String beanName = toLowerFirstCase(clazz.getSimpleName());
Object instance = clazz.newInstance();
//保存在ioc容器
iocMap.put(beanName, instance);
System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");
}else if(clazz.isAnnotationPresent(XService.class)){
String beanName = toLowerFirstCase(clazz.getSimpleName());
//如果注解包含自定义名称
XService xService = clazz.getAnnotation(XService.class);
if("".equals(xService.value())){
beanName = xService.value();
}
Object instance = clazz.newInstance();
iocMap.put(beanName, instance);
System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");
//找类的接口(把该类所依赖的接口全都注入)
for(Class<?> i :clazz.getInterfaces()){
if(iocMap.containsKey(i.getName())){
throw new Exception("The Bean Name is Exist.");
}
iocMap.put(i.getName(), instance);
System.out.println("[INFO-3] {" + i.getName() + "} has been saved in iocMap.");
}
}
}
}catch(Exception e){
}
}
这里的注入都是通过添加注解来实现的,那么需要判断哪些类被添加了注解(如@Service,@Controller等),如果有这些注解,则需要将这个类注入到容器中。对应的就是在iocMap中添加这个类就行了。如果在注解中指定了value,那么可以将这个value值作为bean的名称,否则就取这个类的类名(将类名的首字母替换为小写)。
2.4、依赖注入
private void doAutowired() {
if(iocMap.isEmpty())
return;
for(Map.Entry<String, Object>entry : iocMap.entrySet()){
//获取每一个class里的字段
Field[]fields = entry.getValue().getClass().getDeclaredFields();
for(Field field: fields){
if(!field.isAnnotationPresent(XAutowired.class)){
continue;
}
//如果该字段上有注解 @Autowired
System.out.println("[INFO-4] Existence XAutowired.");
//获取注解对应的类
XAutowired xAutowired = field.getAnnotation(XAutowired.class);
String beanName = xAutowired.value().trim();
//获取XAutowired注解的值
if("".equals(beanName)){
System.out.println("[INFO] xAutowired.value() is null");
//如果注解没有值,则取该字段的字段名
beanName = field.getType().getName();
}
//只要加了注解,都需要加载,不管是private 还是protect
field.setAccessible(true);
try{
//entry.getValue()获取的是类对象, icoMap.get(beanName)获取的是需要被注入的类的对象
//理解起来就是,给controller中被注入的那个字段赋值。
field.set(entry.getValue(), iocMap.get(beanName));
System.out.println("[INFO-4] field set {" + entry.getValue() + "} - {" + iocMap.get(beanName) + "}.");
}catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
所谓依赖注入,在这里就是指的是解析@Autowired注解。@Autowired标注在变量上,我们需要从IOC容器中,取出对应的实例,赋给这个变量。
例如:
@XAutowired
XTestService testService;
需要再在iocMap容器中,找到XTestService的实例,将其赋给testService这个变量,那么在controller中,就可以调用testService这个服务。
2.5、初始化url与方法的映射
//5、初始化handlerMapping
private void initHandlerMapping() {
if(iocMap.isEmpty()){
return;
}
for(Map.Entry<String, Object> entry : iocMap.entrySet()){
Class<?>clazz = entry.getValue().getClass();
//必须要有controller标签
if(!clazz.isAnnotationPresent(XController.class)){
continue;
}
String baseUrl = "";
if(clazz.isAnnotationPresent(XRequestMapping.class)){
XRequestMapping xRequestMapping = clazz.getAnnotation(XRequestMapping.class);
baseUrl = xRequestMapping.value();
}
for(Method method: clazz.getMethods()){
if(!method.isAnnotationPresent(XRequestMapping.class)){
continue;
}
XRequestMapping xRequestMapping = method.getAnnotation(XRequestMapping.class);
String url = ("/" + baseUrl + "/" + xRequestMapping.value()).replaceAll("/+","/");
handlerMapping.put(url, method);
System.out.println("[INFO-5] handlerMapping put {" + url + "} - {" + method + "}.");
}
}
}
将@XRequestMapping注解中的value值与所在的方法对应起来,当有请求来的时候,可以通过请求的url来调用相应的方法。
3、处理请求
初始化之后,就可以处理浏览器发来的请求了。
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//7、运行阶段
try {
doDispach(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exception Detail:
" + Arrays.toString(e.getStackTrace()));
}
}
private void doDispach(HttpServletRequest request, HttpServletResponse response) throws InvocationTargetException, IllegalAccessException {
String url = request.getRequestURI();
String contextPath = request.getContextPath();
url = url.replaceAll(contextPath, "").replaceAll("/+","/");
System.out.println("[INFO]request url ----> "+url);
if(!(this.handlerMapping.containsKey(url))){
try {
response.getWriter().write("404 NOT FOUND");
return;
} catch (IOException e) {
e.printStackTrace();
}
return;
}
//获取url对应的方法
Method method = this.handlerMapping.get(url);
System.out.println("[INFO]method ----> "+method);
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
System.out.println("[INFO]iocMap.get(beanName)->" + iocMap.get(beanName));
//第一个参数是获取方法,后面的参数,多个参数直接加,按顺序对应
method.invoke(iocMap.get(beanName),request, response);
System.out.println("[INFO] method.invoke put {" + iocMap.get(beanName) + "}.");
}
通过url,来调用相应的方法,传入 request和response对象。
4、编写测试类
@XController
@XRequestMapping("/test")
public class TestController {
@XAutowired
XTestService testService;
@XRequestMapping("/getUserInfo")
public void getUserInfo(HttpServletRequest request, HttpServletResponse response){
String userInfo = testService.getUserInfo();
try {
response.getWriter().write(userInfo);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@XService
public class XTestServiceImpl implements XTestService {
@Override
public String getUserInfo() {
return "admin:123";
}
}
5、配置tomcat并启动,浏览器中访问:
http://localhost:8080/test/getUserInfo