手写SpringMVC底层机制

手写 SpringMVC 底层机制

  1. 前景提要:实现的是SpringMVC核心机制

  2. 对一些细枝末节的代码做了简化,比如字符串的处理...

  3. 完成哪些机制

    • 机制一: 通过@RequestMapping ,可以标记一个方法,编写路径url,浏览器就能通过url完成调用
    • 机制二: 进行依赖注入,使之不需要传统的new 一个对象,而是直接从IOC容器中获得
    • 机制三:通过@RequestParam,如果浏览器传递的参数名和目标方法的形参不一致,可以通过value设置进行匹配
    • 机制四:在目标方法完成后,跳转到相关页面 请求转发/重定向
    • 机制五:在目标方法完成后,通过@Response注解,向浏览器发送JSON格式数据

手写添加配置

思路

  1. 需要配置pom.xml的依赖
  2. 需要写一个Servlet 作为前端控制器
  3. 需要配置Web.xml 中的前端控制器 1).url 2)配置spring容器配置文件的classpath: 3)跟随Tomcat 自启动
  4. 需要配置spring容器配置文件
  5. 需要配置spring容器配置文件 扫描的路径 <component-scan ...>

实现

  • 需要配置pom.xml的依赖
<!--  配置原生Servlet-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    <!--  <scope> 表示引入的jar的作用范围
          provided 表示该项目在打包 放到生产环境时,不需要带上 servlet-api.jar
          因为tomcat本身有 servlet-api.jar,到时直接使用tomcat本身的 servlet-api.jar-->
    </dependency>

  <!--  配置dom4j-->
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.1</version>
    </dependency>

  <!--  配置常用的工具类-->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.12.0</version>
    </dependency>
  • 需要写一个Servlet 作为前端控制器
public class ZyDispatcherServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("ZyDispatcherServlet-doPost--");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("ZyDispatcherServlet-doGet--");
    }
}
  • 需要配置Web.xml 中的前端控制器 1).url 2)配置spring容器配置文件的classpath: 3)跟随Tomcat 自启动

  • 需要配置spring容器配置文件 扫描的路径 <component-scan ...>

 <servlet>
    <servlet-name>ZyDispatcherServlet</servlet-name>
    <servlet-class>com.code_study.zyspringmvc.servlet.ZyDispatcherServlet</servlet-class>
    
    <!--配置参数,指定要操作的spring配置文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:zyspringmvc.xml</param-value>
    </init-param>

    <!--跟随tomcat自启动-->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>ZyDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
  • 需要配置spring容器配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <!--    要扫描的包-->
    <component-scan base-package="com.code_study.controller,com.code_study.service"></component-scan>
</beans>

完成浏览器可以请求控制层

思路

  • 创建@Controller和自己的Controller
  • 编写工具类XMLParser,解析spring容器配置文件
  • 开发自己的 Spring容器,得到扫描类的全路径列表
  • 开发自己的 前端控制器,实例化对象到容器中
  • 完成请求的URL和控制器方法的映射关系
  • 完成前端控制器分发请求到对应控制器
  1. 自定义注解@Controller
  2. 创建Controller
  3. 需要写一个工具类XMLParser来解析在spring容器配置文件 扫描的路径 <component-scan ...> 的包 返回所有的路径
  4. 这个所有的路径 一个split(",")分隔,都进行扫描
  5. 需要写自己的 前端控制器
  6. 需要写自己的 Spring容器
  7. 在前端控制器中 需要添加方法 scanPackage() 扫描 XMLParser 解析出来的路径
  8. 在Spring容器中 需要添加一个属性 classFullPathList 来保存扫描出来的类的全路径
  9. 需要添加一个属性 ioc 来存放反射生成的bean对象 也就是过滤classFullPathList 中没有@Controller注解的一些路径 并实例化
  10. 需要添加类Handler 这个类要保存 一个url 对应的 一个控制器的方法的 映射 ,也就是说,根据这个url,可以找到对应控制器的对应方法
  11. 需要添加一个属性 HandlerList 用于 保存Handler 【url 和 控制器的映射】
  12. 需要添加三个方法 一个是initHandlerMapping(),完成 url 对应的 一个控制器的方法的 映射,即 将ioc 中bean 中的 方法进行反射,获取url,将 url,method,bean 封装成Handler 放入HandlerList 保存
  13. 添加第二个方法 getHandler(),需要将浏览器发送的request请求中的 uri拿出来,遍历HandlerList 进行配对,如果有 就返回对应的Handler
  14. 添加第三个方法 executeDispatch(),进行分发处理,需要 调用getHandler() 获取浏览器发送的request请求 对应的 Handler ,获取Handler 中的method 进行反射调用,method .invoke() 实现分发请求。

实现

  1. 自定义注解@Controller
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}
  1. 创建Controller
@Controller
public class MonsterController {
}
  1. 需要写一个工具类XMLParser来解析在spring容器配置文件 扫描的路径 <component-scan ...> 的包 返回所有的路径
public class XMLParser {

    public static String getBasePackage(String xmlFile){
        SAXReader saxReader = new SAXReader();
        ClassLoader classLoader = XMLParser.class.getClassLoader();
        InputStream resourceAsStream = classLoader.getResourceAsStream(xmlFile);
        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();
            Element element = rootElement.element("component-scan");
            String basePackage = element.attribute("base-package").getText();
            return basePackage;
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. 这个所有的路径 一个split(",")分隔,都进行扫描
  2. 需要写自己的 前端控制器
  3. 需要写自己的 Spring容器
  4. 在前端控制器中 需要添加方法 scanPackage() 扫描 XMLParser 解析出来的路径
public void scanPackage(String pack) {
        //获得包所在的工作路径 [绝对路径]
        URL url =
                this.getClass().getClassLoader().//获取类的加载器
                        //得到指定包对应的工作路径 [绝对路径]
                                getResource("/" + pack.replaceAll("\\.", "/"));

        // System.out.println("url= "+url);
        //根据得到的路径,对其进行扫描,把类的全路径 保存到 classFullPathList
        String path = url.getFile();
        //在io中 把目录也是为一个文件
        File file = new File(path);
        //遍历file 【遍历出文件和子目录】
        for (File f : file.listFiles()) {
            if (f.isDirectory()) {//如果是目录
                //需要递归扫描 找子目录
                scanPackage(pack + "." + f.getName());
            } else {
                //的确是个文件
                //扫描到的文件可能是 .class 文件 也可能是其他文件
                //就算是.class 文件 也需要判断是否需要注入容器 有无加 @Controller注解
                //目前无法拿到注解 因为没法反射 所以先把文件的全路径都保存到 classFullPathList 之后在注入对象到容器时再处理
                String classFullPath =
                        //类的全路径不需要.class 去掉.class
                        pack + "." + f.getName().replaceAll(".class", "");

                //保存到 classFullPathList
                classFullPathList.add(classFullPath);
            }
        }
    }
  1. 在Spring容器中 需要添加一个属性 classFullPathList 来保存扫描出来的类的全路径
//保存扫描的包/子包类的全路径
    private List<String> classFullPathList =
            new ArrayList<>();
  1. 需要添加一个属性 ioc 来存放反射生成的bean对象 也就是过滤classFullPathList 中没有@Controller注解的一些路径 并实例化
//定义属性 ioc -> 存放反射生成的bean对象 比如Controller / Service /Dao
public ConcurrentHashMap<String, Object> ioc =
        new ConcurrentHashMap<>();
  1. 编写方法,将扫描到的类,在满足情况下 反射到ioc容器
//编写方法,将扫描到的类,在满足情况下 反射到ioc容器
    public void executeInstance() {
        if (classFullPathList.size() == 0) {
            //说明没有扫描到类
            return;
        }

        //遍历classFullList
        for (String classFullPath : classFullPathList) {
            try {
                Class<?> clazz = Class.forName(classFullPath);
                if (clazz.isAnnotationPresent(Controller.class)) {//处理@Controller
                    String className = clazz.getSimpleName();

                    Object instance = clazz.newInstance();
                    String value = clazz.getAnnotation(Controller.class).value();
                    if (!"".equals(value)) {
                        className = value;
                    } else {
                        className = StringUtils.uncapitalize(className);
                    }
                    ioc.put(className, instance);
                }
                else if (clazz.isAnnotationPresent(Service.class)) {//处理@Service
                    String className = clazz.getSimpleName();//类名

                    Service serviceAnnotation = clazz.getAnnotation(Service.class);
                    String value = serviceAnnotation.value();

                    if (!"".equals(value)) {
                        className = value;
                        Object instance = clazz.newInstance();
                        ioc.put(className, instance);
				}
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
  1. 需要添加类Handler 这个类要保存 一个url 对应的 一个控制器的方法的 映射 ,也就是说,根据这个url,可以找到对应控制器的对应方法
ZyHandler {
    private String url;
    private Method method;
    private Object controller;

    public ZyHandler() {
    }

    public ZyHandler(String url, Method method, Object controller) {
        this.url = url;
        this.method = method;
        this.controller = controller;
    }
//需要提供getter和setter方法...
  1. 需要添加一个属性 HandlerList 用于 保存Handler 【url 和 控制器的映射】
//定义属性 HandlerList -> 保存ZyHandler 【url 和 控制器的映射】
    private List<ZyHandler> HandlerList =
            new ArrayList<>();
  1. 需要添加三个方法 一个是initHandlerMapping(),完成 url 对应的 一个控制器的方法的 映射,即 将ioc 中bean 中的 方法进行反射,获取url,将 url,method,bean 封装成Handler 放入HandlerList 保存
 private void initHandlerMapping(){
        //遍历 ioc
        for (Map.Entry<String,Object> entry:  zyWebApplicationContext.ioc.entrySet()) {
            if (zyWebApplicationContext.ioc.isEmpty()){
                return;
            }
            Object bean = entry.getValue();
            Class<?> clazz = bean.getClass();
            if (clazz.isAnnotationPresent(Controller.class)){
                Method[] declaredMethods = clazz.getDeclaredMethods();
                for (Method declaredMethod : declaredMethods) {
                    if (declaredMethod.isAnnotationPresent(RequestMapping.class)){

                        String url = declaredMethod.getAnnotation(RequestMapping.class).value();

                        ZyHandler zyHandler = new ZyHandler(url, declaredMethod, bean);

                        HandlerList.add(zyHandler);
                    }
                }
            }
        }
    }
  1. 添加第二个方法 getHandler(),需要将浏览器发送的request请求中的 uri拿出来,遍历HandlerList 进行配对,如果有 就返回对应的Handler
 //编写方法,通过request对象 返回ZyHandler对象 ,如果没有返回null
    private ZyHandler getZyHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        //遍历HandlerList
        for (ZyHandler zyHandler : HandlerList) {
            if (requestURI.equals(zyHandler.getUrl())){
                return zyHandler;
            }
        }
        return null;
    }
  1. 添加第三个方法 executeDispatch(),进行分发处理,需要 调用getHandler() 获取浏览器发送的request请求 对应的 Handler ,获取Handler 中的method 进行反射调用,method .invoke() 实现分发请求。
   public void executeDispatch(HttpServletRequest request,HttpServletResponse response){
        ZyHandler zyHandler = getZyHandler(request);
        try {
            if (null == zyHandler){
               response.getWriter().write("<h1>404 NOT FOUND</h1>");
            }
            Method method = zyHandler.getMethod();
            method.invoke(zyHandler.getController(),request,response);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

从web.xml文件中动态获取spring配置文件

思路

  1. 首先,我们自己写的前端控制器是一个Servlet,它有 servletConfig,可以servletConfig.getInitParameter("xxx") 来获取之前在web.xml配置的classpath:
  2. 工具类XMLParser 是在spring容器中解析的 ,web.xml配置的classpath: 是在前端控制器中获取的,因此需要spring容器提供有参构造器,在前端控制器添加参数 spring容器,将classpath 传到spring容器中进行解析。

实现

  1. 首先,我们自己写的前端控制器是一个Servlet,它有 servletConfig,可以servletConfig.getInitParameter("xxx") 来获取之前在web.xml配置的classpath:
  2. 工具类XMLParser 是在spring容器中解析的 ,web.xml配置的classpath: 是在前端控制器中获取的,因此需要spring容器提供有参构造器,在前端控制器添加参数 spring容器,将classpath 传到spring容器中进行解析。
@Override
    public void init() throws ServletException {
        String configLocation = getServletConfig().getInitParameter("contextConfigLocation");
        System.out.println("ZyDispatcherServlet 初始化---");
        zyWebApplicationContext = new ZyWebApplicationContext(configLocation);
        zyWebApplicationContext.init();

        initHandlerMapping();
        System.out.println("HandlerList= "+HandlerList);

    }
 private  String configLocation;
    public ZyWebApplicationContext(String configLocation) {
        this.configLocation = configLocation;
    }
 public void init(){
        System.out.println("ZyWebApplicationContext 初始化---");
        String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
        String[] basePackages = basePackage.split(",");
        if (basePackages.length >0) {
            for (String  pack : basePackages) {
                scanPackage(pack);
            }
        }
 }

@Service注解

思路

  1. @Service注解是写在类上的 即@Target(ElementType.TYPE)
  2. 这个注解标识的类就是一个Service,那么同样是在executeInstance()方法中判断是否有注解@Service,有的话就保存到ioc容器中
  3. 由于是Service,那么保存进ioc 的 k-v 中的 k 就有三种。第一种,就是默认值,用接口的类型的名字首字母小写;第二种,在@Service注解种设置了value属性,那么 k = value;第三章,需要用类名首字母小写也可以获取bean

实现

  1. @Service注解是写在类上的 即@Target(ElementType.TYPE)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
    String value() default "";
}
  1. 这个注解标识的类就是一个Service,那么同样是在executeInstance()方法中判断是否有注解@Service,有的话就保存到ioc容器中
  2. 由于是Service,那么保存进ioc 的 k-v 中的 k 就有三种。第一种,就是默认值,用接口的类型的名字首字母小写;第二种,在@Service注解种设置了value属性,那么 k = value;第三章,需要用类名首字母小写也可以获取bean
 public void executeInstance() {
        if (classFullPathList.size() == 0){
            return;
        }
        //遍历 classFullPathList
        for (String classFullPath : classFullPathList) {
            try {
                Class<?> clazz = Class.forName(classFullPath);

                if (clazz.isAnnotationPresent(Controller.class)){
                    Controller controller = clazz.getAnnotation(Controller.class);
                    String value = controller.value();
                    String className = clazz.getSimpleName();
                    Object instance = clazz.newInstance();
                    if ("".equals(value)){
                        className = StringUtils.uncapitalize(className);
                    }else {
                        className = value;
                    }
                    ioc.put(className,instance);
                } else if (clazz.isAnnotationPresent(Service.class)) {
                    Service serviceAnnotation = clazz.getAnnotation(Service.class);
                    String annoattionValue = serviceAnnotation.value();
                    Object instance = clazz.newInstance();

                    if ("".equals(annoattionValue)){
                        Class<?>[] interfaces = clazz.getInterfaces();
                        for (Class<?> anInterface : interfaces) {
                            String simpleName = anInterface.getSimpleName();
                            simpleName = StringUtils.uncapitalize(simpleName);
                            ioc.put(simpleName,instance);
                        }

                        //可以通过类名首字母小写
                        String simpleName = clazz.getSimpleName();
                        simpleName = StringUtils.uncapitalize(simpleName);
                        ioc.put(simpleName,instance);

                    }else {
                        ioc.put(annoattionValue,instance);
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

@Autowried 依赖注入

思路

  1. @Autowried注解是用在字段上,通过ioc容器自动装配,因此 @Target(ElementType.FIELD)
  2. 需要遍历ioc中所有的 bean 中的所有字段,来进行判断是否需要自动装配
  3. 通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值
  4. value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");
  5. 如果有值,去ioc容器进行查找该值对应的bean,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");如果有该bean,就用该字段的set()方法将本身的bean 和 查找ioc获取的bean进行装配
  6. 即 declaredField.set(bean, beanInIOC);
  7. 由于字段是私有属性private,需要暴力破解declaredField.setAccessible(true);

实现

  1. @Autowried注解是用在字段上,通过ioc容器自动装配,因此 @Target(ElementType.FIELD)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    String value() default "";
}
  1. 需要遍历ioc中所有的 bean 中的所有字段,来进行判断是否需要自动装配

  2. 通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值

  3. value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");

  4. 如果有值,去ioc容器进行查找该值对应的bean,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");如果有该bean,就用该字段的set()方法将本身的bean 和 查找ioc获取的bean进行装配

  5. 即 declaredField.set(bean, beanInIOC);

  6. 由于字段是私有属性private,需要暴力破解declaredField.setAccessible(true);

public void  executeAutoWired(){
    //遍历ioc
    if (ioc.isEmpty()){
        return;
    }

    //获取容器里的所有bean 以及 bean对应的字段
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        Object bean = entry.getValue();

        Class<?> clazz = bean.getClass();
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field declaredField : declaredFields) {

            //通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值
            if (declaredField.isAnnotationPresent(Autowired.class)){
                Autowired annotation =
                        declaredField.getAnnotation(Autowired.class);
                String beanName = annotation.value();

                try {
                    //value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,
                    if ("".equals(beanName)){
                        beanName = declaredField.getType().getSimpleName();
                        beanName = StringUtils.uncapitalize(beanName);

                    }
                        Object iocBean = ioc.get(beanName);
                    //如果没有抛出空指针异常
                        if (null == iocBean){
                            throw new NullPointerException("ioc 没有该Bean");
                        }
                        declaredField.setAccessible(true);//暴力破解
                        declaredField.set(bean,iocBean);

                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

@RequestParam

思路

  1. @RequestParam 是写在PARAMETER上的 也就是说 @Target(ElementType.PARAMETER)
  2. 这个注解实现的功能 大方向是 分发请求 也就是说 写在 executeDispatch()方法中的
  3. 要实现 @RequestParam 需要将形参列表封装到一个数组中 , 因为反射 invoke 可以传入一个数组作为可变参数解析
  4. 那么就需要两个数组 一个数组是新创建的数组(大小应与,目标数组的形参个数一致) 将实参放入进去 ;另一个是 反射拿到的目标方法的形参的数组
  5. 这两个数组的内容需要一一对应,因为反射需要顺序一致
  6. 需要获取到request 中请求的参数Map 获取参数名和参数值
  7. 可以将功能具体化到 完成这个新数组的实参填写 【完成新数组之后放入invoke方法进行反射就行】
  8. 将步骤拆解成 1)完成HttpServletRequest 和 HttpServletResponse 的填写 ;2)完成带有@RequestParam注解的形参的填写;3)完成普通的方法参数没有@RequestParam注解的形参的填写
  9. 完成HttpServletRequest 和 HttpServletResponse 的填写:就需要先拿到目标方法的所有参数。根据类型的名称 和 形参列表进行匹配 填写
  10. 完成带有@RequestParam注解的形参的填写:需要添加方法,获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返回int 代表第几个参数,如果有@RequestParam注解,就会返回相应的索引,如果没有就会返回-1 进行普通方法参数的填写 处理
  11. 普通方法参数的 填写:需要添加方法,将目标方法的所有的形参的名称 反射保存到 List 返回,再通过遍历判断 请求的参数名 和 List中一致的 填写到 数组中
  12. 普通方法参数的 填写 需要插件,使用java8的特性 解决在默认情况下 parameter.getName() 获取的名字不是形参真正的名字,而是[arg0,arg1,arg2...]的问题

实现

  1. @RequestParam 是写在PARAMETER上的 也就是说 @Target(ElementType.PARAMETER)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";
}
  1. 这个注解实现的功能 大方向是 分发请求 也就是说 写在 executeDispatch()方法中的
  2. 要实现 @RequestParam 需要将形参列表封装到一个数组中 , 因为反射 invoke 可以传入一个数组作为可变参数解析
Method method = zyHandler.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();//形参数组
Object[] params = new Object[parameterTypes.length];
  1. 那么就需要两个数组 一个数组是新创建的数组(大小应与,目标数组的形参个数一致) 将实参放入进去 ;另一个是 反射拿到的目标方法的形参的数组
  2. 这两个数组的内容需要一一对应,因为反射需要顺序一致
  3. 需要获取到request 中请求的参数Map 获取参数名和参数值
 request.setCharacterEncoding("utf-8");
 Map<String, String[]> parameterMap = request.getParameterMap();
 for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
    String name = entry.getKey();//参数 键
    String value = entry.getValue()[0];//参数 值
  1. 可以将功能具体化到 完成这个新数组的实参填写 【完成新数组之后放入invoke方法进行反射就行】

  2. 将步骤拆解成 1)完成HttpServletRequest 和 HttpServletResponse 的填写 ;2)完成带有@RequestParam注解的形参的填写;3)完成普通的方法参数没有@RequestParam注解的形参的填写

  3. 完成HttpServletRequest 和 HttpServletResponse 的填写:就需要先拿到目标方法的所有参数。根据类型的名称 和 形参列表进行匹配 填写

 for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> parameterType = parameterTypes[i];
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                        params[i] = request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i] = response;
                    }
                }
  1. 完成带有@RequestParam注解的形参的填写:需要添加方法,获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返回int 代表第几个参数,如果有@RequestParam注解,就会返回相应的索引,如果没有就会返回-1 进行普通方法参数的填写 处理
 //获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返
 private int getRequestParamterIndex(Method method,String name){
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                if (parameter.isAnnotationPresent(RequestParam.class)) {

                    String value = parameter.getAnnotation(RequestParam.class).value();

                    if (name.equals(value)) {
                        return i;//返回int 代表第几个参数
                    }
                }
        }
        return -1;
    }
int requestParamterIndex = getRequestParamterIndex(zyHandler.getMethod(), name);
                    if (requestParamterIndex != -1) {
                        params[requestParamterIndex] = value;
                    } 
  1. 普通方法参数的 填写:需要添加方法,将目标方法的所有的形参的名称 反射保存到 List 返回,再通过遍历判断 请求的参数名 和 List中一致的 填写到 数组中
	//将目标方法的所有的形参的名称 反射保存到 List 返回
    private List<String> getParameterNames(Method method){
        List<String> parameterList = new ArrayList<>();

        //获取到所有的参数名称
        Parameter[] parameters = method.getParameters();

        for (Parameter parameter : parameters) {
            //在默认情况下 parameter.getName() 获取的名字不是形参真正的名字
            //而是[arg0,arg1,arg2...]
            //需要插件,使用java8的特性 解决
            String name = parameter.getName();
            parameterList.add(name);
        }
        System.out.println("目标方法的形参列表=" + parameterList);
        return parameterList;
    }
else {
                        //没找到@RequestParam 对应参数--使用默认机制
                        //1. 得到目标方法的所有形参名
                        //2. 对得到目标方法的所有形参名进行遍历,
                        //如果匹配就把当前请求的参数值放入params
                        List<String> parameterNames = getParameterNames(zyHandler.getMethod());
                        for (int i = 0; i < parameterNames.size(); i++) {
                            if (name.equals(parameterNames.get(i))) {
                                params[i] = value;
                                break;
                            }
                        }
                    }
  1. 普通方法参数的 填写 需要插件,使用java8的特性 解决在默认情况下 parameter.getName() 获取的名字不是形参真正的名字,而是[arg0,arg1,arg2...]的问题
  <!--可以进行json操作-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.14.0</version>
    </dependency>

  • 完整代码
 public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
        ZyHandler zyHandler = getZyHandler(request);
        try {
            if (null == zyHandler) {
                response.getWriter().write("<h1>404 NOT FOUND!</h1>");
            } else {
                Method method = zyHandler.getMethod();
                Class<?>[] parameterTypes = method.getParameterTypes();//形参数组
                Object[] params = new Object[parameterTypes.length];


                //遍历 parameterTypes
                //获取 HttpServletRequest , HttpServletResponse 在形参数组中的位置
                //将request 和 response 保存到  params相应的位置上
                for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> parameterType = parameterTypes[i];
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                        params[i] = request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i] = response;
                    }
                }

                //获取request中的
                request.setCharacterEncoding("utf-8");
                Map<String, String[]> parameterMap = request.getParameterMap();
                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                    String name = entry.getKey();//参数 键
                    String value = entry.getValue()[0];//参数 值

                    //获取形参数组中 带有@RequestParam 的形参 的位置
                    //将带有@RequestParam 保存到  params相应的位置上
                    int requestParamterIndex = getRequestParamterIndex(zyHandler.getMethod(), name);
                    if (requestParamterIndex != -1) {
                        params[requestParamterIndex] = value;
                    } else {
                        //没找到@RequestParam 对应参数--使用默认机制
                        //1. 得到目标方法的所有形参名
                        //2. 对得到目标方法的所有形参名进行遍历,
                        //如果匹配就把当前请求的参数值放入params
                        List<String> parameterNames = getParameterNames(zyHandler.getMethod());
                        for (int i = 0; i < parameterNames.size(); i++) {
                            if (name.equals(parameterNames.get(i))) {
                                params[i] = value;
                                break;
                            }
                        }
                    }
                }

视图解析

思路

  1. 首先 视图解析 它是在分发请求后 在目标方法中进行视图的跳转 forward 或者 redirect,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
  2. 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
  3. 当返回的是String类型后 我们就可以根据 splic(":")进行分隔
  4. splic(":")[0] 就是进行跳转的方式 forward 或者 redirect
  5. splic(":")[1] 就是进行跳转的页面
  6. 如果没有":" ,就说明是默认情况,forward 处理即可

实现

  1. 首先 视图解析 它是在分发请求后 在目标方法中进行视图的跳转 forward 或者 redirect,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用

  2. 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理

    Object result = zyHandler.getMethod().invoke(zyHandler.getController(), params);
    
  3. 当返回的是String类型后 我们就可以根据 splic(":")进行分隔

  4. splic(":")[0] 就是进行跳转的方式 forward 或者 redirect

  5. splic(":")[1] 就是进行跳转的页面

  6. 如果没有":" ,就说明是默认情况,forward 处理即可

 if (result instanceof String){
                   String viewName =  (String) result;
                    if (viewName.contains(":")) {
                        String viewType = viewName.split(":")[0];
                        String viewPage = viewName.split(":")[1];

                        if ("forward".equals(viewType)){
                            request.getRequestDispatcher(viewPage).forward(request,response);
                        }else if (("redirect".equals(viewType))){
                            response.sendRedirect(viewPage);
                        }
                    }else {
                        request.getRequestDispatcher(viewName).forward(request,response);
                    }
                }

@ResponseBody 返回JSON数据

实现

  1. 首先@ResponseBody 是写在方法上的 因此 @Target(ElementType.METHOD)
  2. 没有默认值 @ResponseBody仅仅作为一个标识
  3. @ResponseBody它是在分发请求后 在目标方法中标识该方法返回JSON格式的数据给浏览器,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
  4. 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
  5. 当返回的是一个集合类型,并且判断该方法有注解@ResponseBody ,就说明是需要向浏览器返回JSON格式数据
  6. 用jackson 包下的 objectWriter对象的 objectMapper.writeValueAsString()方法 ,可以很轻松的将集合转化为JSON进行返回
  7. 不要忘了设置response格式,防止乱码,response.setContentType("text/html;charset=utf-8");
  8. 直接用response.getWriter.writer()将转化后的结果返回给浏览器即可

思路

  1. 首先@ResponseBody 是写在方法上的 因此 @Target(ElementType.METHOD)
  2. 没有默认值 @ResponseBody仅仅作为一个标识
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
  1. @ResponseBody它是在分发请求后 在目标方法中标识该方法返回JSON格式的数据给浏览器,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
  2. 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
  3. 当返回的是一个集合类型,并且判断该方法有注解@ResponseBody ,就说明是需要向浏览器返回JSON格式数据
  4. 用jackson 包下的 objectWriter对象的 objectMapper.writeValueAsString()方法 ,可以很轻松的将集合转化为JSON进行返回
else if (result instanceof ArrayList) {
    if (zyHandler.getMethod().isAnnotationPresent(ResponseBody.class)) {
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(result);
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(json);
        writer.flush();
        writer.close();
    }
}
  1. 不要忘了设置response格式,防止乱码,response.setContentType("text/html;charset=utf-8");
  2. 直接用response.getWriter.writer()将转化后的结果返回给浏览器即可
posted @ 2024-04-24 14:33  zy2596  阅读(92)  评论(0编辑  收藏  举报