使用Java元注解和反射实现简单MVC框架

Springmvc的核心是DispatcherServlet来进行各种请求的拦截,进而进行后续的各种转发处理。流程图如下:

 

  说明:客户端发出一个http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet的请求映射路径(在web.xml中指定),web容器将请求转交给DispatcherServlet.DipatcherServlet接收到这个请求之后将根据请求的信息(包括URL、Http方法、请求报文头和请求参数Cookie等)以及HandlerMapping的配置找到处理请求的处理器(Handler)。DispatcherServlet根据HandlerMapping找到对应的Handler,将处理权交给Handler(Handler将具体的处理进行封装),再由具体的HandlerAdapter对Handler进行具体的调用。Handler对数据处理完成以后将返回一个ModelAndView()对象给DispatcherServlet。Handler返回的ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherSevlet通过 ViewResolver将逻辑视图转化为真正的视图View。Dispatcher通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view并返回给客户端。

 

以下基于java元注解和反射等知识来实现简单MVC框架。

1、Servlet

Servlet 3.0 之前使用web.xml文件进行配置,例如:

  <servlet>
        <serlvet-name>myServlet</servlet-name>
        <servlet-calss>MyServlet的类路径</servlet-class>
    </servlet>
    
    <servlet-mapping>
      <serlvet-name>myServlet</servlet-name>
      <url-pattern>/servlet/myServlet</url-pattern>
    </servlet-mapping>

Servlet 3.0 后可以基于注解来处理Servlet

@WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1,
            initParams = {@WebInitParam(name = "base-package", value = "com.kinson.myspring")})

name:servlet名

urlPatterns:url匹配模式

loadOnStartup:标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法),它的值必须是一个整数,表示servlet应该被载入的顺序.

  1.   当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet
  2.   当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载
  3.   正数的值越小,该servlet的优先级越高,应用启动时就越先加载
  4.   当值相同时,容器就会自己选择顺序来加载 

initParams:初始化参数,此处表示定义了一个名为base-package,值为com.kinson.myspring的WebInitParam对象,可以通过ServletConfig的getInitParameter("base-package");方法获取对应的值。

2、实现

2.1 工程目录结构

相关代码说明:

  1. 在 annotation 包下,我将提供自定义的注解,为了方便理解,会与 Spring MVC 保持一致。JDK 元注解介绍

  2. 为了模拟 Spring MVC 的方法调用链,我这里提供 Controller/Service/Dao 层进行测试。

  3. 提供自定义的 DispatcherServlet 来完成核心逻辑处理。

具体代码实现:

2.2 pom引入servlet依赖

    <!--项目依赖-->
    <dependencies>
        <!-- servlet 依赖-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${servlet.version}</version>
        </dependency>
        
    </dependencies>

2.3 Annotation

以Controller为例,其他的注解类似:

//用于类、接口、枚举enum
@Target(ElementType.TYPE)
//生命周期为运行时
@Retention(RetentionPolicy.RUNTIME)
//javadoc
@Documented
public @interface Controller {

    /**
     * 作用于该注解的一个value属性
     * @return
     */
    String value();
}

2.4 请求拦截类DispatcherServlet :

定义相关全局存储变量:

// @WebServlet 以前我们定义一个 Servlet ,需要在 web.xml 中去配置,不过在 Servlet 3.0 后出现了基于注解的 Servlet 。
@WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1,
        initParams = {@WebInitParam(name = "base-package", value = "com.kinson.myspring")})
public class DispatcherServlet extends HttpServlet {
    /**
     * 扫描的包
     */
    private String basePackage = "";

    /**
     * 基包下面所有的带包路径权限定类名
     */
    private List<String> packageNames = new ArrayList<String>();

    /**
     * 注解实例化 格式为注解上的名称:注解实例化对象
     */
    private Map<String, Object> instanceMap = new HashMap<String, Object>();

    /**
     * 包路径权限定类名称:注解上的名称
     */
    private Map<String, String> nameMap = new HashMap<String, String>();

    /**
     * Url地址和方法的映射关系:注解上的名称
     */
    private Map<String, Method> urlMethodMap = new HashMap<String, Method>();

    /**
     * Method和权限定类名的映射关系,用于通过Method找到该方法的对象利用反射执行
     */
    private Map<Method, String> methodPackageMap = new HashMap<Method, String>();
}

初始化方法init:

  /**
     * 初始化
     * 1、扫描基包下的类,得到信息 A。
     * 2、对于 @Controller/@Service/@Repository 注解而言,我们需要拿到对应的名称,并初始化它们修饰的类,形成映射关系 B。
     * 3、扫描类中的字段,如果发现有 @Qualifier 的话,我们需要完成注入。
     * 4、扫描 @RequestMapping,完成 URL 到某一个 Controller 的某一个方法上的映射关系 C。
     *
     * @param config
     */
    @Override
    public void init(ServletConfig config) {
        System.out.println("开始初始化。。。。。。");

        //通过初始化参数直接将需要扫描的基包路径传入
        basePackage = config.getInitParameter("base-package");

        try {
            //扫描基包得到全部的带包路径权限定类名
            scanBasePackage(basePackage);
            //把代用注解的类实例化方如Map中,key为注解上的名称
            instance(packageNames);
            //IOC注入
            springIOC();
            //完成Url地址与方法的映射关系
            handleUrlMethodMap();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        System.out.println("初始化结束。。。。。。");
    }

方法scanBasePackage通过初始化参数直接将需要扫描的基包路径传入:

  /**
     * 通过初始化参数直接将需要扫描的基包路径传入
     *
     * @param basePackage 基包路径
     */
    private void scanBasePackage(String basePackage) {
        //加载类资源路径
        URL url = this.getClass().getClassLoader()
                .getResource(basePackage.replaceAll("\\.", "/"));

        File basePackageFile = new File(url.getPath());
        System.out.println("scan:" + basePackageFile);
        File[] childFiles = basePackageFile.listFiles();
        for (File file : childFiles) {
            //目录递归扫描
            if (file.isDirectory()) {
                scanBasePackage(basePackage + "." + file.getName());
            } else if (file.isFile()) {
                //Controller.class====Controller,即去掉.class
                System.out.println(">>>>>>>>>>> " + file.getName() + "====" + file.getName().split("\\.")[0]);
                packageNames.add(basePackage + "." + file.getName().split("\\.")[0]);
            }
        }
    }

方法instance把代用注解的类实例化方如Map中,key为注解上的名称:

/**
     * 把代用注解的类实例化方如Map中,key为注解上的名称
     *
     * @param packageNames 包路径名集合
     */
    private void instance(List<String> packageNames) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if (packageNames.size() < 1) {
            return;
        }

        for (String packageName : packageNames) {
            //根据包路径获取Class对象
            Class<?> clazz = Class.forName(packageName);
            //Controller注解处理
            if (clazz.isAnnotationPresent(Controller.class)) {
                Controller controller = (Controller) clazz.getAnnotation(Controller.class);
                String controllerName = controller.value();

                instanceMap.put(controllerName, clazz.newInstance());
                nameMap.put(packageName, controllerName);

                System.out.println("Controller :" + packageName + ", value :" + controllerName);
            } else if (clazz.isAnnotationPresent(Service.class)) {
                //Service注解处理
                Service service = (Service) clazz.getAnnotation(Service.class);
                String serviceName = service.value();

                instanceMap.put(serviceName, clazz.newInstance());
                nameMap.put(packageName, serviceName);

                System.out.println("Service :" + packageName + ", value :" + serviceName);
            } else if (clazz.isAnnotationPresent(Repository.class)) {
                //Repository注解处理
                Repository repository = clazz.getAnnotation(Repository.class);
                String repositoryName = repository.value();

                instanceMap.put(repositoryName, clazz.newInstance());
                nameMap.put(packageName, repositoryName);
                System.out.println("Repository :" + packageName + ", value :" + repositoryName);
            }
        }
    }

方法springIOC注入:

/**
     * IOC注入
     */
    private void springIOC() throws IllegalAccessException {
        for (Map.Entry<String, Object> instanceEntry : instanceMap.entrySet()) {
            //获取当前对象的所有字段
            Field[] declaredFields = instanceEntry.getValue().getClass().getDeclaredFields();

            for (Field field : declaredFields) {
                //字段上是否有Qualifier注解
                if (field.isAnnotationPresent(Qualifier.class)) {
                    Qualifier qualifier = field.getAnnotation(Qualifier.class);
                    String qualifierName = qualifier.value();

                    //设置当前的字段为可访问
                    field.setAccessible(Boolean.TRUE);
                    //设置当前字段
                    field.set(instanceEntry.getValue(), instanceMap.get(qualifierName));

                    System.out.println("==========" + field);
                }
            }
        }
    }

方法handleUrlMethodMap处理Url地址与方法的映射关系:

/**
     * Url地址与方法的映射关系
     *
     * @throws ClassNotFoundException
     */
    private void handleUrlMethodMap() throws ClassNotFoundException {
        if (packageNames.size() < 1) {
            return;
        }

        for (String packageName : packageNames) {
            //根据包路径获取Class对象
            Class clazz = Class.forName(packageName);

            //当前类是否有Controller注解
            if (clazz.isAnnotationPresent(Controller.class)) {
                //获取当前Controller类的所有方法
                Method[] methods = clazz.getMethods();
                //拼接访问URI
                StringBuffer baseUrl = new StringBuffer();

                //当前Controller是否有RequestMapping注解
                if (clazz.isAnnotationPresent(RequestMapping.class)) {
                    RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
                    //XxxController类上的requestMapping值
                    baseUrl.append(requestMapping.value());
                }

                for (Method method : methods) {
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        //XxxController类上的响应方法上的requestMapping值
                        baseUrl.append(requestMapping.value());

                        //URL 提取出来,映射到 Controller 的 Method 上。
                        System.out.println("baseUrl : " + baseUrl.toString());
                        urlMethodMap.put(baseUrl.toString(), method);
                        methodPackageMap.put(method, packageName);
                    }
                }
            }
        }
    }

doGet/doPost方法处理拦截请求的业务逻辑:

   @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) {
        doPost(req, resp);
    }

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) {

        //获取请求URI,eg:/user/hello
        final String uri = req.getRequestURI();
        final String contextPath = req.getContextPath();
        final String path = uri.replaceAll(contextPath, "");

        //提取出 URL,通过 URL 映射到Method 上,然后通过反射的方式进行调用即可。
        Method method = urlMethodMap.get(path);
        if (null != method) {
            //通过方法获取方法所在的包路径
            String packageName = methodPackageMap.get(method);
            //通过包路径获取注解上的名称
            String controllerName = nameMap.get(packageName);
            //通过注解名称获取对应的实例对象
            UserController userController = (UserController) instanceMap.get(controllerName);

            try {
                //设置方法可访问
                method.setAccessible(Boolean.TRUE);
                //利用反射进行方法调用
                method.invoke(userController);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

测试UserController类:

@Controller("userController")
@RequestMapping("/user")
public class UserController {

    @Qualifier("userServiceImpl")
    private UserService userService;

    @RequestMapping(value = "/hello")
    public String hello() {
        System.out.println("UserController.hello");
        return "UserController.hello";
    }
}

测试UserService接口:

public interface UserService {

    void hello();
}

测试UserServiceImpl接口:

@Service("userServiceImpl")
public class UserServiceImpl implements UserService {

    @Override
    public void hello() {
        System.out.println("hello, myspring");
    }
}

3、测试

配置tomcat运行项目,此处我用的是idea,具体配置:

选择本地安装的tomcat:

设置相关内容:

点击右下角的Fix按钮选择部署包:

配置好之后运行,在浏览器输入测试url:

idea控制台打印了内容:

到此,一个简单的MVC框架就ok了。

 

Github源码参照

 

posted @ 2019-04-01 13:38  花拾夕  阅读(961)  评论(0编辑  收藏  举报