Bootstrap

手写Spring-MVC初版 准备工作

Day46

手写Spring-MVC

解决Controller层的方案

思路:监听器在项目启动时DispatherServlet会将controller层的信息记录下来,当前端发送请求的时候DispatherServlet就会根据信息分发给controller层。

准备工作

在这里插入图片描述

准备工作的目的是准备好监听器,而监听器的具体实现是获取controlle层的controller类的信息(用类描述类来封装这些信息,并将URI和描述类整合到集合map中)。

步骤1:自定义注解-作用是包含路径信息、URI、标识controller类等。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Configuration {
    String value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

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

步骤2:DispatcherServlet如何记录controller层的信息?

-在web.xml配置文件中配置好config配置参数,这个参数可以直接通过请求的getInitParameter()方法获得,于是可以在web项目模块中写一个配置文件的类Appconfig,在web.xml配置文件中直接写明这个配置文件类的路径,这样在项目启动时可以直接访问到这个类。

而写配置文件类的目的是为了通过这个配置文件类拿到web项目中controller层的信息(路径),这样在java模块中就可以访问到controller层了。(具体实现是利用反射,拿到类对象,再拿到注解里写好的路径)。

在这一步中需要写好配置类以及配置文件:

package com.qf.shop.web.config;

import com.qf.shop.mvc.annotation.Configuration;

/**
 * 当前项目的配置类
 */
@Configuration("com.qf.shop.web.controller")
public class AppConfig {
}

配置类中是第一步自定义的configuration注解,此注解的目的就是给出当前web项目中controller层的路径。

<context-param>
        <param-name>config</param-name>
        <param-value>com.qf.shop.web.config.AppConfig</param-value>
    </context-param>

步骤3: 为创建监听器准备条件,即写好类描述类,而来描述类中又需要方法描述类,方法描述类中需要参数描述类集合。

package com.qf.shop.mvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 类描述类
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class BeanDefinition<T> {
    private String requestMappingPath;//父级URi
    private Class<?> clazz;//Controller类的class对象
    private String name;//类名
    private T t;//Controller类对象
    private MethodDefinition methodDefinition;//方法描述类对象
}

package com.qf.shop.mvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.lang.reflect.Method;
import java.util.List;

/**
 * 方法描述类
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MethodDefinition {

    private String requestMappingPath;//子级URi
    private String name;//方法名
    private Method method;//方法对象

    private Class<?> returnClazz;//返回值类型
    private List<ParameterDefinition> parameterDefinitions;//参数描述类对象的集合
}

package com.qf.shop.mvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class ParameterDefinition {
    private String name;//参数名
    private Class<?> clazz;//参数类型
    private int index;//参数下标

}

注意:此处用到了lombok插件,该插件提供了通过注解自动编写有参、无参、getset方法及toString方法的功能。可以在IDEA中的file->settings->plugin中搜索下载。

步骤4:因为可能出现配置信息有误、配置信息残缺、class文件转换异常、uri映射错误、requestMapping配置错误等等情况,所以写一个错误码接口及其实现枚举类;

对于所有可能出现的异常写一个异常类,继承RuntimeException非受检性异常,抛出枚举类中对应的错误码和错误信息。

又因为监听器最后的目的是将信息存储到集合中,这些信息应该是共享的,所以要将map放进一个静态类的静态map属性中。

错误码接口及其实现类:

package com.qf.shop.mvc.constant;

/**
 * 错误码的接口
 */
public interface ResponseCodeInterface {
    int getCode();
    void setCode(int code);
    String getMessage();
    void setMessage(String message);
}

package com.qf.shop.mvc.constant;

public enum ResponseCode implements ResponseCodeInterface{

    CONFIG_EXCEPTION(100,"config的配置信息出错"),
    CONFIGURATION_EXCEPTION(101,"需要配置Configuration这个注解"),
    CLASS_FILE_EXCEPTION(102,"class文件转换异常"),
    REQUEST_MAPPING_PATH_EXCEPTION(103,"RequestMapping地址设置有误"),
    REQUEST_PATH_EXCEPTION(104,"uri映射错误"),
    EXCEPTION_CONFIG_EXCEPTION(105,"未配置全局异常的路径"),
    ADVISER_CONFIG_EXCEPTION(106,"未配置处理器的路径");

    private int code;
    private String message;

    ResponseCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public int getCode() {
        return code;
    }

    @Override
    public void setCode(int code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    @Override
    public void setMessage(String message) {
        this.message = message;
    }
}

异常信息类:

package com.qf.shop.mvc.exception;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class FrameWorkException extends RuntimeException{
    private int code;
    private String message;
}

容器类:

package com.qf.shop.mvc.container;

import com.qf.shop.mvc.model.BeanDefinition;

import java.util.HashMap;

public class TypeContainer {
    private static HashMap<String, BeanDefinition> maps = null;
    static{
        maps=new HashMap<>();
    }

    public static HashMap<String, BeanDefinition> getMaps() {
        return maps;
    }

    public static void setMaps(HashMap<String, BeanDefinition> maps) {
        TypeContainer.maps = maps;
    }
}

步骤5:创建监听器。

思路:通过config配置参数获取配置文件类对象,然后获取其注解信息,通过信息找到controller层路径,但是此时不知道controller层下面的类信息。所以要先通过全局域对象获取项目发布的绝对路径,然后和层拼接成层的绝对路径,再封装为File对象,利用listFiles()函数获取文件下面的子文件对象,整合到列表中,再对每个file文件更改路径,利用反射再次转换回Class对象,这样就获得了controller层下面的类对象。

获得类对象后对于每个类对象遍历,依次获得类描述类、方法描述类、参数描述类(多层for循环)的属性,然后进行封装,再调用静态容器整合。

package com.qf.shop.mvc.listerner;

import com.qf.shop.mvc.annotation.Configuration;
import com.qf.shop.mvc.annotation.Controller;
import com.qf.shop.mvc.annotation.RequestMapping;
import com.qf.shop.mvc.constant.ResponseCode;
import com.qf.shop.mvc.container.TypeContainer;
import com.qf.shop.mvc.exception.FrameWorkException;
import com.qf.shop.mvc.model.BeanDefinition;
import com.qf.shop.mvc.model.MethodDefinition;
import com.qf.shop.mvc.model.ParameterDefinition;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;


public class ApplicationListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        /*
         * 扫描web项目的Controller层
         * 封装成类描述类的对象
         * 添加到容器中 -- Key(父级uri+子级uri) Value(类描述类的对象)
         */

        System.out.println("项目启动");

        //获取web项目的配置文件全路径
        ServletContext servletContext = sce.getServletContext();
        String config = servletContext.getInitParameter("config");
        if(config==null){
            throw new FrameWorkException(ResponseCode.EXCEPTION_CONFIG_EXCEPTION.getCode(),ResponseCode.EXCEPTION_CONFIG_EXCEPTION.getMessage());
        }

        //获取配置文件的class对象
        Class<?> configClass = getConfigClass(config);

        //获取扫描Controller层的位置
        String controllerPosition = getControllerPosition(configClass);

        //拼接Web项目中Controller层的绝对路径
        String filePath = getControllerPath(servletContext, controllerPosition);

        //获取controller层的所有文件:fileList
        List<File> fileList= new ArrayList<>();
        getFileList(filePath,fileList);

        //将fileList转换为classList
        List<Class<?>> classList = tramToClassList(servletContext, fileList);

        //将类对象封装为描述类对象,并添加到容器中
        handlerClassList(classList);

        Set<Map.Entry<String, BeanDefinition>> entries = TypeContainer.getMaps().entrySet();
        for (Map.Entry<String, BeanDefinition> entry : entries) {
            System.out.println(entry);
        }

    }
    public void handlerClassList(List<Class<?>> classList){

        for (Class<?> clazz : classList) {
            try {
                String clazzName = clazz.getName();//类名
                Object t = clazz.newInstance();//类对象
                RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
                if(requestMapping==null){
                    throw new FrameWorkException(ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getCode(),ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getMessage());
                }
                String fatherUri = requestMapping.value();//父级URI
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods) {
                    method.setAccessible(true);
                    String methodName = method.getName();//方法名
                    Class<?> returnType = method.getReturnType();//返回值类型
                    RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class);
                    if(methodAnnotation==null){
                        throw new FrameWorkException(ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getCode(),ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getMessage());
                    }
                    String sonUri = methodAnnotation.value();//获取子级URI
                    List<ParameterDefinition> parameterList = new ArrayList<>();
                    Parameter[] parameters = method.getParameters();
                    if(parameters.length!=0){
                        for (int i = 0; i < parameters.length; i++) {
                           String parameterName = parameters[i].getName();//获取参数名
                           Class<?> parameterType = parameters[i].getType();//参数类型
                           int index = i;//下标
                           ParameterDefinition parameterDefinition = new > >ParameterDefinition(parameterName, parameterType, index);//封装参数描述类对象
                           parameterList.add(parameterDefinition);
                      }
                    }
                    MethodDefinition methodDefinition = new MethodDefinition(sonUri, methodName, method, returnType, parameterList);//封装方法描述类对象
                    BeanDefinition<Object> beanDefinition = new BeanDefinition<>(fatherUri, clazz, clazzName, t, methodDefinition);//封装类描述类对象
                    String key = fatherUri+sonUri;//拼接uri
                    HashMap<String, BeanDefinition> map = TypeContainer.getMaps();
                    map.put(key,beanDefinition);


//                    Set<Map.Entry<String, BeanDefinition>> entries = map.entrySet();
//                    for (Map.Entry<String, BeanDefinition> entry : entries) {
//                        System.out.println(entry);
//                    }

                }

            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public List<Class<?>> tramToClassList(ServletContext servletContext,List<File> fileList){
        String realPath = servletContext.getRealPath("WEB-INF\\classes");
        ArrayList<Class<?>> classList = new ArrayList<>();
        for (File file : fileList) {
            //F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes\com\qf\shop\web\controller\A.class
            String fileAbsolutePath = file.getAbsolutePath();
            //\com\qf\shop\web\controller\A.class
            fileAbsolutePath = fileAbsolutePath.replace(realPath, "");
            //com\qf\shop\web\controller\A
            fileAbsolutePath = fileAbsolutePath.substring(1,fileAbsolutePath.lastIndexOf("."));
            //com.qf.shop.web.controller.A
            String name = fileAbsolutePath.replace("\\", ".");
            try {
                Class<?> clazz = Class.forName(name);
                Annotation annotation = clazz.getAnnotation(Controller.class);
                if(annotation!=null){
                    classList.add(clazz);
                }
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }

        }
        return classList;
    }

    public void getFileList(String filePath,List<File> fileList){
        File file = new File(filePath);
        File[] files = file.listFiles();
        for (File afile : files) {
            if(afile.isDirectory()){
                String s = afile.getAbsolutePath();
                getFileList(s,fileList);
            }else if(afile.isFile()){
                fileList.add(afile);
            }
        }
    }


    public String getControllerPath(ServletContext servletContext,String controllerPosition){

        //F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes
        String realPath = servletContext.getRealPath("WEB-INF\\classes");//发布路径中的编译后的.class文件路径
        //com\qf\shop\web\controller
        controllerPosition = controllerPosition.replaceAll("\\.", "\\\\");
        //F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes/com\qf\shop\web\controller
        String filePath = realPath + File.separator +controllerPosition;
        return filePath;
    }

    public String getControllerPosition(Class<?> configClass){
        Configuration configClassAnnotation = configClass.getAnnotation(Configuration.class);
        if(configClassAnnotation==null){
            throw new FrameWorkException(ResponseCode.CONFIGURATION_EXCEPTION.getCode(),ResponseCode.CONFIGURATION_EXCEPTION.getMessage());

        }
        String controllerPosition = configClassAnnotation.value();
        return controllerPosition;
    }
    public Class<?> getConfigClass(String config){

        Class<?> appConfigClass = null;
        try {
            appConfigClass = Class.forName(config);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return appConfigClass;
    }



}

注意:由于JVM在编译时不会将方法名编译进class文件中,所以需要maven-compiler-plugin插件,作用:将参数名编译进class文件。

写完监听器后别忘了在web.xml中进行配置

<listener>
        <listener-class>com.qf.shop.mvc.listerner.ApplicationListener</listener-class>
    </listener>

可能遇到的问题

1.发布路径报错

解决方案:将发布路径改为Tomcat的webApps下的ROOT根路径

2.监听器没有工作

解决方案:在web项目的pom.xml中添加java框架项目的依赖

;