【Spring】仿写Spring核心逻辑--实现 IOC、DI、MVC

在开始仿写前,我们先明确要实现的三个目标:

目标一:IOC。可以通过自定义注解@MYService@MYController,将对象交给IOC容器管理

public interface IDemoService {
	String get(String name);
}

@MYService
public class DemoService implements IDemoService {

	public String get(String name) {
		return "My name is " + name;
	}
}

注意,只是标了注解还不够,我们还需要通过配置文件appliacation.properties去配置要扫描的类在哪些包

在这里插入图片描述
目标二:DI。可以通过自定义注解@MYAutowired实现依赖注入

@MYController
public class DemoAction {
  	@MYAutowired
	private IDemoService demoService;
}

目标三:MVC。可以通过自定义注解@MYRequestMapping@MYRequestParam实现类似SpringMVC的请求分发与处理

@MYController
@MYRequestMapping("/demo")
public class DemoAction {

  	@MYAutowired
	private IDemoService demoService;

	@MYRequestMapping("/query")
	public void query(HttpServletRequest req, HttpServletResponse resp,
                      @MYRequestParam("name") String name){
		String result = demoService.get(name);
//		String result = "My name is " + name;
		try {
			resp.getWriter().write(result);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@MYRequestMapping("/add")
	public void add(HttpServletRequest req, HttpServletResponse resp,
					@MYRequestParam("a") Integer a, @MYRequestParam("b") Integer b){
		try {
			resp.getWriter().write(a + "+" + b + "=" + (a + b));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@MYRequestMapping("/remove")
	public void remove(HttpServletRequest req, HttpServletResponse resp,
                       @MYRequestParam("id") Integer id){
	}
}

项目的整体结构如下:

那下面我们就开始动手实现上面的三个目标!

1.自定义注解

根据上面的要求,我们首先要做的就是自定义五个注解:
在这里插入图片描述

@Target(ElementType.FIELD) // @Autowired是用在成员变量上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYAutowired {
    String value() default "";
}
@Target({ElementType.TYPE}) // @Controller是用在类上的
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYController {
    String value() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD}) // @RequestMapping既可以用在类上,也可以用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYRequestMapping {
    String value() default "";
}
@Target({ElementType.PARAMETER}) // @RequestParam是用在方法的参数上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYRequestParam {
    String value() default "";
}
@Target(ElementType.TYPE) // @Service是用在类上的
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYService {
    String value() default "";
}

更多关于自定义注解的内容可以参考这篇 【Java杂记】注解:自定义注解示例

2.核心类 MYDispatchServlet

实现上面三个要求的代码都在MYDispatchServlet类中,那这个类该怎么写呢?我们可以从目标三这个终极目标入手,就跟 Springmvc 一样,我们也写一个DispatchServlet,然后在初始化时完成任务一(IOC)和任务二(DI)。

// 继承javax.HttpServlet 
public class MYDispatchServlet extends HttpServlet {
	
	// 初始化阶段,在容器创建Servlet时会执行初始化方法(init)
    @Override
    public void init(ServletConfig config) throws ServletException {
        // 1.加载配置文件
        // 传入配置文件路径
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        
        // 2.扫描相关类
        // 传入配置文件中的scanPackage的包名
        doScanner(contextConfig.getProperty("scanPackage"));
        
        // 3.初始化扫描到的类,并将它们放入IOC容器
        doInstance();

        // 4.完成依赖注入
        doAutowired();

        // 5.初始化HandlerMappring
        initHandlerMapping();

        System.out.println("MYSpring framework is init");
    }

    // 运行阶段
    // 处理post请求
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 请求分发,将请求分发个相应方法
        // 注:此处,该方法还包含了分发后方法的执行
        try {
            doDispatch(req, resp);
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 Excetion Detail:" +Arrays.toString(e.getStackTrace()));
        }
    }
	
	// 处理get请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
}

架子搭好了,下面就对上面定义的方法一一实现:

2.1 doLoadConfig()

private Properties contextConfig = new Properties();

// 读取配置文件信息到内存
private void doLoadConfig(String contextConfigLocation) {
    // 通过当前类的ClassLoader读取Properties文件,成IO流;相当于读入了 scanPackage=com.yzh.demo 到内存中
    InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
    try {
        // 存到Properties对象中
        contextConfig.load(fis);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.2 doScanner()

private List<String> classNames = new ArrayList<String>();

// 扫描指定包,得到指定包下所有类的全类名,本质是文件操作
private void doScanner(String scanPackage) {
    // 1.获取需要扫描包的File对象
    // 这里不能直接new File,因为new File需要的是相对路径或绝路径,
    // 而上一步得到的包名什么都不是,所以我们的策略是先获取URL对象
    // 注:getResource方法返回的是URL对象,用来获取指定类或包的绝对路径
    //    getResource获取绝对路径,首先要将包名转化成项目相对路径。
    //    而最前面 / 表示从根路径中寻找(显然要找到这个包不能从当前目录下寻找)
    URL url = this.getClass().getClassLoader().
            getResource("/" + scanPackage.replaceAll("\\.", "/"));
	// 通过getFile获取到要扫描包的绝对路径(文件夹)
    File classpath = new File(url.getFile());

    // 2.遍历文件夹,寻找class文件
    for (File file : classpath.listFiles()) {
        if (file.isDirectory()) {
            // 这里是通过递归遍历文件夹,还是包就再执行上述步骤(解析路径->创建目录->遍历)
            doScanner(scanPackage + "." + file.getName());
        } else {
            // 不是class文件的不管
            if (!file.getName().endsWith("class")) {continue;}
            // 这里要保存全类名(包.类名),因为后面要通过反射Class.forName获取Class对象
            String className = (scanPackage + "." + file.getName()).replace(".class", "");
            classNames.add(className);
        }
    }
}

2.3 doInstance()

private Map<String, Object> ioc = new HashMap<String, Object>();

// 将需要IOC容器管理的对象放入IOC容器
private void doInstance() {
	// 如果指定要扫描的包(文件夹)下没有文件
    if (classNames.isEmpty()) {return;}

    try {
        for (String className : classNames) {
            Class<?> clazz = Class.forName(className);
            // 1.标了@MYController的类的对象需要IOC容器管理
            if (clazz.isAnnotationPresent(MYController.class)) {
            	// 通过Class对象创建相应的实例对象!
                Object instance = clazz.newInstance();
                // 这里getSimpleName是只获取类名,而IOC容器的beanName只要单纯的类名就够了
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                ioc.put(beanName, instance);
             // 2.标了@MYService的类的对象需要IOC容器管理
            } else if (clazz.isAnnotationPresent(MYService.class)) {
                Object instance = clazz.newInstance();
                // 2.1 按类名注入(最常见的方式),一般需要类名小写(驼峰规则)
                // 注:这里明确一个问题,classNames中存的是全类名,而这里要做beanName的只能是类名
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                // 2.2 按自定义beanName注入
                // 注:这里就是注解的基本操作,拿到注解,然后通过注解中定义的方法拿到参数
                MYService annotation = clazz.getAnnotation(MYService.class);
                if (!"".equals(annotation.value())) {
                    beanName = annotation.value();
                }

                ioc.put(beanName, instance);
                // 2.3 除了上面按类名注入外,还要考虑接口。就像我们平常注入Service一样,声明时都是直接声明的接口。
                // 注:这里还要考虑多实现的问题,即一个对象可能不止实现了一个接口,所以需要遍历(getInterfaces)
                for (Class<?> i : clazz.getInterfaces()) {
                    // 如果一个接口已经有实现对象的Bean了,抛异常
                    if (ioc.containsKey(i.getName())) {
                        throw new Exception("The beanName is existed!!!");
                    }
                    ioc.put(i.getName(), instance);
                }
            // 3.没有注解(或配置)的类,IOC容器不管;
            } else {
                // 接着看下一个类
                continue;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 这里默认是只传入类名大写的,A=65, a=97
private String toLowerFirstCase(String simpleName) {
    char[] chars = simpleName.toCharArray();
    chars[0] += 32;
    return new String(chars);
}

2.4 doAutowired()

// 对已经被管理的对象们进行依赖注入
private void doAutowired() {
    // IOC容器为空,表示没有能进行依赖注入的对象
    if (ioc.isEmpty()) { return; }

    // 遍历IOC容器
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        // 1.拿到当前对象所有field(注:Declared表示也能拿到private成员)
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        // 2.遍历所有field们
        for (Field field : fields) {
            // 2.1 判断这些filed中们有没有需要注入的(有@MYAtowired)
            if (field.isAnnotationPresent(MYAutowired.class)) {
                // 2.2 获取要注入的beanName
                MYAutowired annotation = field.getAnnotation(MYAutowired.class);
                // 2.2.1 自定义beanName
                String beanName = annotation.value().trim();
                if ("".equals(beanName)) {
                    // 2.2.2 若没有自定义beanName,就以字段的类名作为beanName
                    // 注:这就体现了上面doInstance对接口进行处理的必要性
                    beanName = field.getType().getName();
                }

                // 3.将IOC中的对象注入当前field
                // setAccessible是为了对public外的字段也能进行注入
                field.setAccessible(true);
                try {
                    field.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }
}

2.5 initHandlerMapping()

private Map<String, Method> handlerMapping = new HashMap<String, Method>();

// 保存方法与请求路径的映射关系(ps:通过method可以获取到所属类名-->根据类名拿到具体的bean)
private void initHandlerMapping() {
    if (ioc.isEmpty()) return;

    // 遍历所有在容器中的对象
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();
        // 不是Controller直接返回
        if (!clazz.isAnnotationPresent(MYController.class)) continue;

        // 获取Controller上的同一请求路径
        String baseUrl = "";
        if (clazz.isAnnotationPresent(MYRequestMapping.class)) {
            MYRequestMapping annotation = clazz.getAnnotation(MYRequestMapping.class);
            baseUrl = annotation.value();
        }

        // 获取Controller中每个方法的请求路径
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            // 不是处理请求的方法就直接返回
            if (!method.isAnnotationPresent(MYRequestMapping.class)) continue;

            MYRequestMapping annotation = method.getAnnotation(MYRequestMapping.class);
            // URL处理:无论写了几个 / 都处理成一个
            String url = ("/" + baseUrl + "/" + annotation.value()).replaceAll("/+", "/");
            handlerMapping.put(url, method);
            // 日志打印能处理的路径
            System.out.println("Mapped" + url + "," + method);
        }
    }
}

2.6 doDispatch

// 请求分发与执行
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    // 获取请求路径
    // 注:getRequestURL:http://localhost:8080/demo/query
    //     getRequestURI: /demo/query
    //     getContextPath: 获取当前Context的统一路径,一般用于一个容器部署多个Context,而此处是" "
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    // url处理:将contextPath去除(应用标识),将多个 / 的替换成一个 /
    url = url.replaceAll(contextPath, "").replaceAll("/+","/");

    // 如果没有处理这个url的method,就返回404
    if (!handlerMapping.containsKey(url)) {
        resp.getWriter().println("404 Not Found!!!");
        return;
    }

    // 获取处理该请求的方法
    Method method = this.handlerMapping.get(url);
    // 获取该method需要的参数类型们(形参列表)
    Class<?>[] parameterTypes = method.getParameterTypes();
    // 用来保存当前方法的具体参数(实参列表)
    Object[] paramValues = new Object[parameterTypes.length];

    // 获取请求中携带的参数映射
    Map<String, String[]> parameterMap = req.getParameterMap();

    // 遍历形参列表,将形参与实参对应(即填充paramValues)
    for (int i = 0; i < parameterTypes.length; i++) {
        Class paramterType = parameterTypes[i];
        // 如果是Request,直接将req放入
        if (paramterType == HttpServletRequest.class) {
            paramValues[i] = req;
            continue;
        // 如果是Response,直接将resp放入
        } else if (paramterType == HttpServletResponse.class) {
            paramValues[i] = resp;
            continue;
         // 如果是String,就要具体寻找其对应的实参了
        } else if (paramterType == String.class) {

            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int j = 0; j < parameterAnnotations.length; j++) {
                for (Annotation a : parameterAnnotations[i]) {
                    // 如果是@RequestParam注解的,就是指定了请求中携带参数名的
                    if (a instanceof MYRequestParam) {
                        // 获取指定的参数名
                        String paramName = ((MYRequestParam)a).value();
                        if (!"".equals(paramName.trim())) {
                            // 在请求Request携带的请求参数map中,获取到对应参数
                            String value = Arrays.toString(parameterMap.get(paramName))
                                    .replaceAll("\\[|\\]","")
                                    .replaceAll("\\s",",");
                            // 放入实参列表
                            paramValues[i] = value;
                        }
                    }
                }
            }
        }
    }
    // 获取具体执行该方法的对象,
    // 注:此处是直接获取方法的所有类的对象
    String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
    // 执行当前方法,传入req,resp,实参列表
    // 注:此处是直接将获取的参数名写死了!!!
    method.invoke(ioc.get(beanName),new Object[]{req, resp, parameterMap.get("name")[0]});
}

这就完了吗?不,servlet还需要配置要一个web.xml,将所有请求都分发到MYDispatchServlet:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
	xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
	version="2.4">
	<display-name>My Web Application</display-name>
	<servlet>
		<servlet-name>mymvc</servlet-name>
		<servlet-class>com.yzh.mvcframework.servlet.MYDispatchServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>application.properties</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>mymvc</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

到此为止,所有代码部分结束。

关于web容器,这里采用的是 jetty,而不是 tomcat,已经提前在pom文件的plugin配置好了,那么我们就来运行一下看看吧!
在这里插入图片描述

运行结果

首先启动jetty,可以看到jetty已经在8080端口成功启动了。
在这里插入图片描述
我们先直接访问 localhost:8080,看看会是什么结果

在这里插入图片描述

跟预期一样,上面doDispatch方法写了如果有匹配到路径就返回 404 not found。那现在我访问之前 DemoAction 里面定义的请求路径 /demo/query/会是什么结果呢?

在这里插入图片描述
完整的代码我放在GitHub上了,需要的同学可以在这里下载…

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值