手写 mini SpringMVC 核心代码

x33g5p2x  于2021-10-09 转载在 Spring  
字(32.6k)|赞(0)|评价(0)|浏览(244)

过程中的可能存在的知识盲区

web.xml中init-param的作用

定制初始化参数:可以定制servlet、JSP、Context的初始化参数,然后可以再servlet、JSP、Context中获取这些参数值。下面拿servlet来举例:

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
  </servlet>

经过上面的配置,在servlet中能够调用getServletConfig().getInitParameter(“contextConfigLocation”)获得参数名对应的值。

web.xml文件的作用及基本配置

获取web.xml中的init-param定义的值

tomcat和servlet快速入门教程!!!

注解@Retention

注解@Retention可以用来修饰注解,是注解的注解,称为元注解。

Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,

这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。

RetentionPolicy有3个值:CLASS RUNTIME SOURCE

按生命周期来划分可分为3类:

1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;

2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;

3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码。

那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。

一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解

如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解

如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解

注解@Override用在方法上,当我们想重写一个方法时,在方法上加@Override,当我们方法的名字出错时,编译器就会报错

注解@Deprecated,用来表示某个类或属性或方法已经过时,不想别人再用时,在属性和方法上用@Deprecated修饰

注解@SuppressWarnings用来压制程序中出来的警告,比如在没有用泛型或是方法已经过时的时候

response.getwriter().println()与response.getwriter.write()

response.getwriter.write()的API如下:

response.getwriter().println()的API如下:

从API中可以看出, 它们两者均可以打印输出各种类型的数据:response.getwriter().println()的功能比response.getwriter.write()的功能更强大一些。

response.getwriter().println()可以输出html类型的标签,还可以输出一个对象。
response.getwriter.write()也可以输出html类型的标签,但它不可以输出一个对象。

request.getRequestURI()相关获取路径的方法区别

假定你的web application 名称(工程名)为news,你在浏览器中输入请求路径:

http://localhost:8080/news/main/list.jsp

则执行下面向行代码后打印出如下结果:

request.getRequestURL() 返回全路径 ,即http://localhost:8080/news/main/list.jsp

1、 System.out.println(request.getContextPath());

打印结果:/news

返回工程名部分,如果工程映射为/,此处返回则为空

2、System.out.println(request.getServletPath());

打印结果:/main/list.JSP

返回除去host和工程名部分的路径

3、 System.out.println(request.getRequestURI());

打印结果:/news/main/list.JSp

返回除去host(域名或者ip)部分的路径

4、 System.out.println(request.getRealPath(“/”));

打印结果:F:\tomcat 6.0\webapps\news\test

返回当前网页的绝对路径

request.getParameterMap()

根据Java规范:request.getParameterMap()返回的是一个Map类型的值,该返回值记录着前端所提交请求中的请求参数和请求参数值的映射关系。这个返回值有个特别之处——只能读。不像普通的Map类型数据一样可以修改。这是因为服务器为了实现一定的安全规范,所作的限制。

对request.getParameterMap()的返回值使用泛型时应该是Map<String,String[]>形式,因为有时像checkbox这样的组件会有一个name对应对个value的时候,所以该Map中键值对是< String–>String[]>的实现。

举例,在服务器端得到jsp页面提交的参数很容易,但通过request.getParameterMap()可以将request中的参数和值变成一个Map。

以下是将得到的参数和值打印出来,形成的map结构:Map(key,value[]),即:key是String型,value是String型数组。

例如:request中的参数t1=1&t1=2&t2=3形成的map结构:

key=t1;value[0]=1,value[1]=2

key=t2;value[0]=3

如果直接用map.get(“t1”),得到的将是:Ljava.lang.String; value只所以是数组形式,就是防止参数名有相同的情况。

反射相关知识要点

  1. 类名.class.getName()的作用是获取这个类的全类名
  2. method.getDeclaringClass()获取当前Method对象所属的Class
  3. class.getDeclaredxxx获取当前类的所有xxx
  4. class.getPackage().getName()获取当前类的包名

Java反射08 : 成员方法Method学习示例

Java反射06 : 成员变量Field学习示例

java URL类接口及简单应用

java URL 获取本地URL new URL(“file:\d:\hehe.html”)

java URL类接口及简单应用

URL的getFile()和getPath()方法

getFile()方法:

public class URLTest {  
    public static void main(String[] args) {  
          
        URL url1 = URLTest.class.getResource("te st.txt");  
        URL url2 = URLTest.class.getResource("中文.txt");  
          
        System.out.println("te st.txt => " + url1.getFile() + ", exist => " + new File(url1.getFile()).exists());  
        System.out.println("中文.txt => " + url2.getFile() + ", exist => " + new File(url2.getFile()).exists());  
    }   
}
te st.txt => /E:/CODE/Test/bin/url/te st.txt, exist => true  
中文.txt => /E:/CODE/Test/bin/url/中文.txt, exist => true

getFile()

getPath()方法

Java通过URL的getpath方法获取的返回路径乱码解决方案

URL的getFile()和getPath()方法的区别

Java中getClassLoader().getResource()和getResource()的区别

Java中getClassLoader().getResource()和getResource()的区别

String中的trim()方法

java.lang.String中的trim()方法的详细说明

Java正则表达式Pattern和Matcher类

Java正则表达式Pattern和Matcher类详解

思路

1.配置文件

  1. 配置application.properties文件

这里出于解析的方便性,用.properties代替spring原生的application.xml

具体配置内容如下:

scanPackage=com.dhy.demo
  1. 配置web.xml文件

所有依赖web容器的项目都是从读取web.xml文件开始的,我们先配置好web.xml文件中的内容

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>Dhy Web Application</display-name>
    <!-- 配置DispatcherServlet-->
    <!--servlet-name标签Servlet程序起一个别名(一般是类名)-->
    <servlet>
        <servlet-name>dhy_mvc</servlet-name>
        <servlet-class>com.dhy.demo.DispatcherServlet</servlet-class>
<!-- 初始化参数-->
        <init-param>
             <param-name>contextConfigLocation</param-name>
             <param-value>application.properties</param-value>
         </init-param>
<!-- 服务器启动创建servlet-->

        <!-- 指定Servlet的创建时机 1.第一次被访问时,创建 <load-on-startup>的值为负数 2.在服务器启动时,创建 <load-on-startup>的值为0或者正整数 -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--给servlet程序配置访问地址-->
<servlet-mapping>
    <!--告诉服务器,我当前配置的请求路径给哪个servlet程序使用-->
    <servlet-name>dhy_mvc</servlet-name>
<!--拦截的路径,/*拦截所有请求,包括所有静态资源,及.jsp-->
<!-- /拦截所有请求,包括所有静态资源,不包括.jsp-->
    <url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

DispatcherServlet是模拟Spring实现的核心功能类

2.自定义注解

//只作用于类上
@Target({ElementType.TYPE})
//保留字节码文件
@Retention(RetentionPolicy.RUNTIME)
@Documented//抽取到api文档中
public @interface Service
{
    //value属性保存当前类在IOC容器中的别名
    //默认为空
    String value() default "";
}

//作用于成员变量上
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired
{
    String value() default "";
}

//作用于类上
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller
{
    //value属性保存当前类在IOC容器中的别名
    //默认为空
    String value() default "";
}


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

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam
{
    String value() default "";
}

3.配置注解

需要引入servlet-api的jar包,这样我们才能使用servlet里面的相关api函数

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

配置业务实现类

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

@Service
public class DemoService implements IDemoService
{
    @Override
    public String get(String name) {
        return "my name is "+name;
    }
}

配置请求类

@Controller
@RequestMapping("/demo")
public class DemoController
{
    @Autowired
    private IDemoService demoService;
    @RequestMapping("/query")
    public void query(HttpServletRequest req, HttpServletResponse resp
                      , @RequestParam("name")String name)
    {
        String ret = demoService.get(name);
         try{
             //向页面输出内容
             resp.getWriter().write(ret);
         }catch(Exception e)
         {
             e.printStackTrace();
         }
    }

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

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

容器初始化

实现1.0版本

所有核心方法逻辑全部写在init()方法中,代码如下:

public class DispatcherServlet extends HttpServlet
{
    //存放处理映射请求的controller容器
    private Map<String,Object> mapping=new HashMap<>();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       //doGet方法实现逻辑和doPost一样,因此这里直接调用doPost方法
        this.doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    }

    private void doDispatch(HttpServletRequest req,HttpServletResponse resp) throws IOException, InvocationTargetException, IllegalAccessException {
        //获取请求的URI
        //工程名+虚拟请求路径
        String url=req.getRequestURI();
        //返回当前工程名称部分
        String contextPath = req.getContextPath();
        //将工程名从url路径中剔除
        url=url.replace(contextPath,"").replaceAll("/+","/");
        //如果容器中没有当前请求的映射的controller
        if(!this.mapping.containsKey(url))
        {
            resp.getWriter().write("404 Not Found!!");
        }
        //获取处理当前请求映射对应的方法
        Method method=(Method)this.mapping.get(url);
        //得到请求参数,封装为map集合
        Map<String, String[]> parameterMap = req.getParameterMap();
        //反射调用方法
        //method.getDeclaringClass()获取当前Method对象所属的Class
        //method.getDeclaringClass().getName()获取当前类的名字(全类名)
        method.invoke(this.mapping.get(method.getDeclaringClass().getName())
                //方法参数有多个
                ,new Object[]{
                        req,resp,parameterMap.get("name")[0]
                });
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        InputStream is=null;
        try {
            //从配置文件中读取出需要扫描的包路径
            Properties configContext=new Properties();
            //从类路径下读取指定的properties文件
            is = this.getClass().getClassLoader().
                    //config.getInitParameter获取初始化参数的值
                            getResourceAsStream(config.getInitParameter("contextConfigLocation"));
            configContext.load(is);
            //获取要扫描的包路径
            String scanPackage = configContext.getProperty("scanPackage");
            //执行包扫描逻辑
            doScanner(scanPackage);
            //遍历容器中的key值集合
            //key存储是全类名
            for (String className : this.mapping.keySet())
            {
                //如果全类名不中含有.,说明不是全类名
                if(!className.contains(".")){continue;}
                //根据全类名,获取其字节码文件对象
                Class<?> clazz=Class.forName(className);
               //判断当前类上是否标注了controller注解
                if(clazz.isAnnotationPresent(Controller.class))
                {
                    //如果当前类上标注了该注解,需要创建唯一实例将其放入容器中
                    //默认类名作为id
                    mapping.put(className,clazz.newInstance());
                    //虚拟请求路径
                    String baseUrl="";
                    //判断当前类上是否标注了requestMapping注解
                    if(clazz.isAnnotationPresent(RequestMapping.class))
                    {
                        //获取这个注解对象
                        RequestMapping requestMapping=clazz.getAnnotation(RequestMapping.class);
                        //如果类上加了注解,并且给了路径,那么就拼接上
                        baseUrl=requestMapping.value();
                    }
                    //得到当前类上的所有方法
                    Method[] methods = clazz.getMethods();
                    for (Method method : methods) {
                        //如果方法上没标注注解,那就直接跳过
                        if(!method.isAnnotationPresent(RequestMapping.class)){continue;}
                        //获取方法上的注解对象
                        RequestMapping requestMapping=method.getAnnotation(RequestMapping.class);
                        //拼接请求路径
                        String url=(baseUrl+"/"+requestMapping.value().replace("/+","/"));
                        //当前方法放入容器中
                        mapping.put(url,method);
                        System.out.println("Mapped "+url+" , "+method);
                    }
                }
                else if(clazz.isAnnotationPresent(Service.class))
                {
                    //获取类上标注的注解对象
                    Service service=clazz.getAnnotation(Service.class);
                    //value值作为bean在容器中的名字
                    String beanName=service.value();
                    //如果没有手动指定bean的名字,那么默认全类名做名字
                    if("".equals(beanName)){beanName=clazz.getName();}
                    //反射创建实例
                    Object instance = clazz.newInstance();
                    //放入容器
                    mapping.put(beanName,instance);
                    //获取当前类实现的所有接口
                    for (Class<?> Interface : clazz.getInterfaces()) {
                             mapping.put(Interface.getName(),instance);
                    }
                }
                else
                {
                    continue;
                }
            }
            //遍历容器中所有的值
            for(Object object:mapping.values())
            {
                if(object==null)continue;
                Class<?> clazz= object.getClass();
                //判断当前类上是否存在controller注解
                if(clazz.isAnnotationPresent(Controller.class))
                {
                    //获取当前类里面定义的所有成员变量,包括私有属性
                    Field[] fields = clazz.getDeclaredFields();
                    for (Field field : fields) {
                        //判断属性上是否有autowired注解
                        if(!field.isAnnotationPresent(Autowired.class))continue;
                        //获取注解对象
                        Autowired autowired = field.getAnnotation(Autowired.class);
                        //获取注入的bean的名字,即按照名字从容器中拿出来,赋值给对应的属性
                        String beanName=autowired.value();
                        //如果beanName没有手动指定
                        //那么beanName默认就是当前属性类型的全类名
                        //因为autowired只能注入引用属性,因此这里得到的就是类的全类名
                        if("".equals(beanName)){beanName=field.getType().getName();}
                        //强制私有属性也可以访问和操作
                        field.setAccessible(true);
                        //给当前属性注入值
                        //第一个参数是当前类的实例对象
                        //第二个参数是要给属性赋的值
                        field.set(mapping.get(clazz.getName()),mapping.get(beanName));
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }finally {
            //关闭流
            if(is!=null)
            {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     //包扫描逻辑
    //扫描指定包下面的所有.class文件,放入容器中,默认类名做id
    private void doScanner(String scanPackage)
    {
        //url保存资源文件相关信息
        URL url=this.getClass().getClassLoader().getResource("/"+scanPackage.replace(".","/"));
        //返回URL文件名部分
        File classDir = new File(url.getFile());
        //遍历当前目录下的所有文件和目录
        for (File file : classDir.listFiles())
        {
            //如果当前是目录
            if(file.isDirectory())
            {
                //递归扫描里面的文件
                doScanner(scanPackage+"."+file.getName());
            }
            else
            {
                //如果是文件,并且是.class结尾的文件
                if(!file.getName().endsWith(".class"))continue;
                //得到当前包下类的全类名
                String clazzName=(scanPackage+"."+file.getName().replace(".class",""));
                //包扫描,容器中只保存当前包下所有类的全类名
                mapping.put(clazzName,null);
            }
        }
    }
}

实现2.0版本

在1.0版本上进行优化,采用常用的设计模式(工厂模式,单例模式,委派模式,策略模式),将init方法中的代码进行封装。

public class DispatchServlet2 extends HttpServlet
{
    //保存application.properties配置文件中的内容
    private Properties contextConfig=new Properties();
    //保存包扫描得到的所有类名
    private  List<String> classNames=new ArrayList<>();
    //IOC容器,这里暂时不考虑并发操作,即不使用concurrentHashMap
    private Map<String,Object> ioc=new HashMap<>();
    //保存url和method的映射关系
    private Map<String, Method> handlerMapping=new HashMap<>();
    //初始化阶段
    @Override
    public void init(ServletConfig config)
    {
        //1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
       //2.包扫描
        doScanner(contextConfig.getProperty("scanPackage"));
       //3.初始化扫描到的类,并将它们放入IOC容器中
        doInstance();
        //4.完成依赖注入
         doAutowired();
         //5.初始化url和method的映射关系
        initHandlerMapping();
        //IOC容器初始化完成
        System.out.println("-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
        System.out.println("Spring frameWork 初始化完成");
        System.out.println("-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
    }

    //加载配置文件
    private void doLoadConfig(String contextConfigLocation)
    {
        //思路: 通过类路径找到spring主配置文件所在路径
        //并且将其读取出来放入properties对象中进行保存
        //这里其实就是将包扫描路径从配置文件中读取出来,然后通过properties对象进行保存
        InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try
        {
            contextConfig.load(fis);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            //别忘记关闭流
          if(null!=fis)
          {
              try
              {
                  fis.close();
              }
              catch (IOException e)
              {
                  e.printStackTrace();
              }
          }
        }
    }

    //包扫描
    private void doScanner(String scanPackage)
    {
       //思路: 这里scanPackage存储的就是需要扫描的包的路径
       //我们这里需要做的就是将其转换为文件的路径,然后遍历当前目录下面所有.class文件
      //得到每个.class文件的类名,放入保存类名的集合中
        URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replace(".","/"));
        //获取当前类路径下的所有.class文件
        //url.getFile()得到的是当前包目录的绝对路径
        File classpath = new File(url.getFile());
        //递归遍历当前的包目录
        for (File file : classpath.listFiles()) {
            if(file.isDirectory())
            {
                //当前目录的名字
                doScanner(scanPackage+"."+file.getName());
            }
            else{
                if(!file.getName().endsWith(".class")){continue;}
                //保存全类名
                String className=scanPackage+"."+file.getName().replace(".class","");
                classNames.add(className);
            }
        }
    }

    //初始化扫描到的类,并将它们放入IOC容器中
    //工厂模式的具体体现
    private void doInstance()
    {
        //初始化,为DI(依赖注入)做准备
        if(classNames.isEmpty()){return;}

            try {

                for (String className : classNames) {
                    Class<?> clazz = Class.forName(className);

                    //这里我们只针对加了controller和service注解的类,才需要进行初始化操作
                    if(clazz.isAnnotationPresent(Controller.class))
                    {
                        //实例化
                        Object Instance = clazz.newInstance();
                        //先判断是否手动指定了当前类实例在容器中对应的id
                        Controller controller = clazz.getAnnotation(Controller.class);
                        String beanName = controller.value().trim();
                        //没有手动指定beanName
                        if(beanName.equals(""))
                        {
                            //Spring默认类名首字母小写,作为IOC容器中对应的id
                            //clazz.getName()获取的是全类名
                            //clazz.getSimpleName()获取的是类名
                            beanName=toLowerFirstCase(clazz.getSimpleName());
                        }
                         //放入ioc容器中
                        ioc.put(beanName,Instance);
                    }
                    else if(clazz.isAnnotationPresent(Service.class))
                    {
                        //1.自定义的beanName
                        Service service=clazz.getAnnotation(Service.class);
                        String beanName = service.value().trim();
                        //2.类名默认是首字母小写
                        if("".equals(beanName)){
                            beanName=toLowerFirstCase(clazz.getSimpleName());
                        }
                        //初始化实例
                        Object instance = clazz.newInstance();
                        //放入ioc容器中
                        ioc.put(beanName,instance);

                        //3.根据类型的自动赋值
                        //例如: 当我们使用service注解的时候,一般autowired的是一个service接口类
                        //那么我们这里获取到的是接口的全类名,如何得到其对应的这里的子类实例呢?
                        //即所有接口的全类名在容器中对应的还是其子类
                        for (Class<?> i : clazz.getInterfaces()) {
                            if(ioc.containsKey(i.getName()))
                            {
                                throw new Exception("The “"+i.getName()+" “is exists!!");
                            }
                            //把接口的类型作为key
                            ioc.put(i.getName(),instance);
                        }
                    }
                    else{continue;}
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

    }

    //自定进行依赖的注入
    private void doAutowired()
    {
        if(ioc.isEmpty()){return;}
        //遍历ioc容器---目前存放的只有指定包下加了controller和service注解的类
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //获取当前类的所有字段,包括私有
            Field[] fields = entry.getValue().getClass().getDeclaredFields();
            //判断字段上是否标注了autowired注解
            for (Field field : fields) {
                if(!field.isAnnotationPresent(Autowired.class)){continue;}
                //获取注解对象
                Autowired autowired = field.getAnnotation(Autowired.class);

                //如果用户没有自定义beanName,默认就根据类型注入
                //即自定义类型的类名小写进行注入
                String beanName=autowired.value().trim();
                //如果自定义了beanName,那就把自定义的beanName作为key去容器中取值
                //这里注入只有service注解的情况,并且service注解使用的时候,都是加在对应的service接口上的
                if("".equals(beanName))
                {
                    //获取接口的类型的全类名作为key,去ioc容器中取值
                    beanName=field.getType().getName();
                }

                //如果是public以外的类型,只要加了autpwired注解都要进行强制赋值
                //反射中叫做暴力访问
                field.setAccessible(true);

                //利用反射机制动态给字段赋值
                try {
                    //第一个参数是当前字段所在类的实例
                    //第二个参数是要赋给当前字段的值
                    field.set(entry.getValue(),ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

     //实现类名的首字母小写
    private String toLowerFirstCase(String simpleName)
    {
        char[] chars=simpleName.toCharArray();
        //大,小写字母的ascii码相差32
        //并且大写字母的ascii小于小写字母
        //在Java中,对char做算术运算实际上就是对ascii码做算术运算
        chars[0]+=32;
        return String.valueOf(chars);
    }


    //实现initHandlerMapping方法,HandlerMapping就是策略模式的应用案例
    //实现url和method的一对一关系
    private void initHandlerMapping()
    {
        if(ioc.isEmpty()){return;}
         //遍历ioc容器
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();

            //如果类上没有controller注解,那么肯定不会存在RequestMapping注解
            if(!clazz.isAnnotationPresent(Controller.class)){continue;}

            //保存类上的RequestMapping("/demo1")注解
            String baseUrl="";
            if(clazz.isAnnotationPresent(RequestMapping.class))
            {
                RequestMapping requestMapping=clazz.getAnnotation(RequestMapping.class);
                baseUrl=requestMapping.value().trim();
            }

            //默认获取所有的public类型方法
            for (Method method : clazz.getMethods()) {
                if(!method.isAnnotationPresent(RequestMapping.class)){continue;}

                RequestMapping requestMapping=method.getAnnotation(RequestMapping.class);

                //拼接虚拟请求资源的url路径
                String url="/"+baseUrl+"/"+requestMapping.value();

                //保存当前url和method的映射关系
                handlerMapping.put(url,method);

                System.out.println("Mapped: "+url+ " , "+method);
            }
        }
    }

    //到此为止,初始化的工作已经完成了
    //实现运行逻辑
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    //doPost()方法中用了委派模式,委派模式的具体体现在doDispatch方法中
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            doDispatch(req,resp);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
             resp.getWriter().write("500 Excetion,Detail: "+Arrays.toString(e.getStackTrace()));
        }
    }

    private void doDispatch(HttpServletRequest req,HttpServletResponse resq) throws IOException, InvocationTargetException, IllegalAccessException {
        //URI ===> 工程名+虚拟路径 ===> /demo1/getAllUsers
        String url=req.getRequestURI();
        //工程名===>/demo1
        String contextPath = req.getContextPath();
        //只保存虚拟路径===>/getAllUsers
        url= url.replace(contextPath, "");
        //判断当前url是否有method能够处理
        if(!handlerMapping.containsKey(url))
        {
            resq.getWriter().write("404 Not Found");
            return;
        }
        //获得能够处理当前url的方法实例对象
        Method method=handlerMapping.get(url);

        //获取请求参数
        Map<String, String[]> parameterMap = req.getParameterMap();

        //加了controller注解的类,在IOC容器中对应的id,要么手动指定,要么是类名小写
        //method.getDeclaringClass()获取当前Method对象所属的Class
        Controller annotation = method.getDeclaringClass().getAnnotation(Controller.class);
        String beanName=annotation.value().trim();
        //没有手动指定beanName
        if("".equals(beanName))
        {
            //默认是类名小写
            beanName=toLowerFirstCase(method.getDeclaringClass().getSimpleName());
        }
        //触发方法
        method.invoke(ioc.get(beanName),new Object[]{req,resq,parameterMap.get("name")[0]});
    }
}

在上面的代码中,doDispatch()虽然完成了动态委派并进行了反射调用,但对url参数的处理还是静态的,要实现url参数的动态获取,还是有些繁琐的,但是我们还是可以对上面的doDispatch方法进行优化:

private void doDispatch(HttpServletRequest req,HttpServletResponse resq) throws IOException, InvocationTargetException, IllegalAccessException {
        //URI ===> 工程名+虚拟路径 ===> /demo1/getAllUsers
        String url=req.getRequestURI();
        //工程名===>/demo1
        String contextPath = req.getContextPath();
        //只保存虚拟路径===>/getAllUsers
        url= url.replace(contextPath, "");
        //判断当前url是否有method能够处理
        if(!handlerMapping.containsKey(url))
        {
            resq.getWriter().write("404 Not Found");
            return;
        }
        //获得能够处理当前url的方法实例对象
        Method method=handlerMapping.get(url);

        //获取请求参数
        Map<String, String[]> parameterMap = req.getParameterMap();
        //获取方法形参列表的参数类型的数组
        Class<?>[] parameterTypes = method.getParameterTypes();
        //保存赋值参数的位置
        Object[] paramValues=new Object[parameterTypes.length];
        //根据参数位置动态赋值
        for(int i=0;i<parameterTypes.length;i++)
        {
            //例如形参是String name===>那么这里parameterType就是String.class
            Class<?> parameterType = parameterTypes[i];
            if(parameterType==HttpServletRequest.class)
            {
                paramValues[i]=req;
            }
            else if(parameterType==HttpServletResponse.class)
            {
                paramValues[i]=resq;
            }
            else if(parameterType==String.class)
            {
                //获取方法中加了注解的参数
                //第一个维度标识的是第i个参数的所有注解
                //第二个维度标识的是第i个参数的第j个注解
                Annotation[][] pa = method.getParameterAnnotations();
                for(int j=0;j<pa.length;j++)
                {
                    //这里每个参数前面最多一个注解
                    for (Annotation a : pa[i])
                    {
                     //自己指定了接收请求参数的名字
                        if(a instanceof RequestParam)
                       {
                        String paramName=((RequestParam) a).value();
                          if(!"".equals(paramName.trim()))
                          {
                              //获取当前请求参数对应的所有值
                              String value = Arrays.toString(parameterMap.get(paramName));
                              //保存参数值
                              paramValues[i]=value;
                          }
                       }
                    }
                }
            }
        }
        //加了controller注解的类,在IOC容器中对应的id,要么手动指定,要么是类名小写
        //method.getDeclaringClass()获取当前Method对象所属的Class
        Controller annotation = method.getDeclaringClass().getAnnotation(Controller.class);
        String beanName=annotation.value().trim();
        //没有手动指定beanName
        if("".equals(beanName))
        {
            //默认是类名小写
            beanName=toLowerFirstCase(method.getDeclaringClass().getSimpleName());
        }
        //触发方法
        method.invoke(ioc.get(beanName),new Object[]{req,resq,parameterMap.get("name")[0]});
    }

3.0版本

在2.0版本中,基本功能已经实现了,但代码的优雅程度还是不太高。例如HandlerMapping还不能像Spring MVC一样支持正则表达式,url参数还不支持强制类型转换,在反射调用前还需要重新获取beanName,在3.0版本中我们继续优化

首先,改造HandlerMapping,在真实的spring源码中,HandlerMapping其实是一个List而非Map,List中的元素是自定义类型的,现在我们来仿真写一段代码,先定义一个内部类Handler:

//Handler记录Controller中的RequestMapping和Mehtod的对应关系
    //内部类--->url和对应method的映射关系保存
    private  class Handler{
        protected Object controller;//保存方法对应的实例
        protected Method method;//保存映射的方法
        protected Pattern pattern;//支持正则
        protected Map<String,Integer> paramIndexMapping;//参数顺序

        protected  Handler(Pattern pattern,Object controller,Method method)
        {
            this.controller=controller;
            this.method=method;
            this.pattern=pattern;
            paramIndexMapping=new HashMap<>();
             //处理当前method方法的参数,并保存参数的属性
            putParamIndexMapping(method);
         }
        //处理并保存参数的顺序
        private void putParamIndexMapping(Method method)
        {
            //提取方法中加了注解的参数
            Annotation[][] pa = method.getParameterAnnotations();
            //第一个维度标识的是第i个参数的所有注解
            //第二个维度标识的是第i个参数的第j个注解
            for(int i=0;i<pa.length;i++)
            {
                for (Annotation a : pa[i]) {
                    if(a instanceof RequestParam)
                    {
                        String paramName = ((RequestParam) a).value();
                        if(!"".equals(paramName.trim()))
                        {
                            paramIndexMapping.put(paramName,i);
                        }
                    }
                }
            }
            //提取方法中的request和response参数
            Class<?>[] parameterTypes = method.getParameterTypes();
            int i=0;
            for (Class<?> parameterType : parameterTypes) {
                if(parameterType==HttpServletRequest.class||parameterType==HttpServletResponse.class)
                {
                    //类名的全类名
                    paramIndexMapping.put(parameterType.getName(),i);
                }
                i++;
            }
        }
    }

然后,优化HandlerMapping的结构,代码如下:

public class DispatchServlet2 extends HttpServlet
{
    //保存application.properties配置文件中的内容
    private Properties contextConfig=new Properties();
    //保存包扫描得到的所有类名
    private  List<String> classNames=new ArrayList<>();
    //IOC容器,这里暂时不考虑并发操作,即不使用concurrentHashMap
    private Map<String,Object> ioc=new HashMap<>();
    //保存url和method的映射关系
    private List<Handler> handlerMapping=new ArrayList<>();
    //初始化阶段
    @Override
    public void init(ServletConfig config)
    {
        //1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
       //2.包扫描
        doScanner(contextConfig.getProperty("scanPackage"));
       //3.初始化扫描到的类,并将它们放入IOC容器中
        doInstance();
        //4.完成依赖注入
         doAutowired();
         //5.初始化url和method的映射关系
        initHandlerMapping();
        //IOC容器初始化完成
        System.out.println("-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
        System.out.println("Spring frameWork 初始化完成");
        System.out.println("-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
    }

    //加载配置文件
    private void doLoadConfig(String contextConfigLocation)
    {
        //思路: 通过类路径找到spring主配置文件所在路径
        //并且将其读取出来放入properties对象中进行保存
        //这里其实就是将包扫描路径从配置文件中读取出来,然后通过properties对象进行保存
        InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try
        {
            contextConfig.load(fis);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            //别忘记关闭流
          if(null!=fis)
          {
              try
              {
                  fis.close();
              }
              catch (IOException e)
              {
                  e.printStackTrace();
              }
          }
        }
    }

    //包扫描
    private void doScanner(String scanPackage)
    {
       //思路: 这里scanPackage存储的就是需要扫描的包的路径
       //我们这里需要做的就是将其转换为文件的路径,然后遍历当前目录下面所有.class文件
      //得到每个.class文件的类名,放入保存类名的集合中
        URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replace(".","/"));
        //获取当前类路径下的所有.class文件
        //url.getFile()得到的是当前包目录的绝对路径
        File classpath = new File(url.getFile());
        //递归遍历当前的包目录
        for (File file : classpath.listFiles()) {
            if(file.isDirectory())
            {
                //当前目录的名字
                doScanner(scanPackage+"."+file.getName());
            }
            else{
                if(!file.getName().endsWith(".class")){continue;}
                //保存全类名
                String className=scanPackage+"."+file.getName().replace(".class","");
                classNames.add(className);
            }
        }
    }

    //初始化扫描到的类,并将它们放入IOC容器中
    //工厂模式的具体体现
    private void doInstance()
    {
        //初始化,为DI(依赖注入)做准备
        if(classNames.isEmpty()){return;}

            try {

                for (String className : classNames) {
                    Class<?> clazz = Class.forName(className);

                    //这里我们只针对加了controller和service注解的类,才需要进行初始化操作
                    if(clazz.isAnnotationPresent(Controller.class))
                    {
                        //实例化
                        Object Instance = clazz.newInstance();
                        //先判断是否手动指定了当前类实例在容器中对应的id
                        Controller controller = clazz.getAnnotation(Controller.class);
                        String beanName = controller.value().trim();
                        //没有手动指定beanName
                        if(beanName.equals(""))
                        {
                            //Spring默认类名首字母小写,作为IOC容器中对应的id
                            //clazz.getName()获取的是全类名
                            //clazz.getSimpleName()获取的是类名
                            beanName=toLowerFirstCase(clazz.getSimpleName());
                        }
                         //放入ioc容器中
                        ioc.put(beanName,Instance);
                    }
                    else if(clazz.isAnnotationPresent(Service.class))
                    {
                        //1.自定义的beanName
                        Service service=clazz.getAnnotation(Service.class);
                        String beanName = service.value().trim();
                        //2.类名默认是首字母小写
                        if("".equals(beanName)){
                            beanName=toLowerFirstCase(clazz.getSimpleName());
                        }
                        //初始化实例
                        Object instance = clazz.newInstance();
                        //放入ioc容器中
                        ioc.put(beanName,instance);

                        //3.根据类型的自动赋值
                        //例如: 当我们使用service注解的时候,一般autowired的是一个service接口类
                        //那么我们这里获取到的是接口的全类名,如何得到其对应的这里的子类实例呢?
                        //即所有接口的全类名在容器中对应的还是其子类
                        for (Class<?> i : clazz.getInterfaces()) {
                            if(ioc.containsKey(i.getName()))
                            {
                                throw new Exception("The “"+i.getName()+" “is exists!!");
                            }
                            //把接口的类型作为key
                            ioc.put(i.getName(),instance);
                        }
                    }
                    else{continue;}
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

    }

    //自定进行依赖的注入
    private void doAutowired()
    {
        if(ioc.isEmpty()){return;}
        //遍历ioc容器---目前存放的只有指定包下加了controller和service注解的类
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //获取当前类的所有字段,包括私有
            Field[] fields = entry.getValue().getClass().getDeclaredFields();
            //判断字段上是否标注了autowired注解
            for (Field field : fields) {
                if(!field.isAnnotationPresent(Autowired.class)){continue;}
                //获取注解对象
                Autowired autowired = field.getAnnotation(Autowired.class);

                //如果用户没有自定义beanName,默认就根据类型注入
                //即自定义类型的类名小写进行注入
                String beanName=autowired.value().trim();
                //如果自定义了beanName,那就把自定义的beanName作为key去容器中取值
                //这里注入只有service注解的情况,并且service注解使用的时候,都是加在对应的service接口上的
                if("".equals(beanName))
                {
                    //获取接口的类型的全类名作为key,去ioc容器中取值
                    beanName=field.getType().getName();
                }

                //如果是public以外的类型,只要加了autpwired注解都要进行强制赋值
                //反射中叫做暴力访问
                field.setAccessible(true);

                //利用反射机制动态给字段赋值
                try {
                    //第一个参数是当前字段所在类的实例
                    //第二个参数是要赋给当前字段的值
                    field.set(entry.getValue(),ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

     //实现类名的首字母小写
    private String toLowerFirstCase(String simpleName)
    {
        char[] chars=simpleName.toCharArray();
        //大,小写字母的ascii码相差32
        //并且大写字母的ascii小于小写字母
        //在Java中,对char做算术运算实际上就是对ascii码做算术运算
        chars[0]+=32;
        return String.valueOf(chars);
    }

    //实现initHandlerMapping方法,HandlerMapping就是策略模式的应用案例
    //实现url和method的一对一关系
    private void initHandlerMapping()
    {
        if(ioc.isEmpty()){return;}
         //遍历ioc容器
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();

            //如果类上没有controller注解,那么肯定不会存在RequestMapping注解
            if(!clazz.isAnnotationPresent(Controller.class)){continue;}

            //保存类上的RequestMapping("/demo1")注解
            String baseUrl="";
            if(clazz.isAnnotationPresent(RequestMapping.class))
            {
                RequestMapping requestMapping=clazz.getAnnotation(RequestMapping.class);
                baseUrl=requestMapping.value().trim();
            }

            //默认获取所有的public类型方法
            for (Method method : clazz.getMethods()) {
                if(!method.isAnnotationPresent(RequestMapping.class)){continue;}
                RequestMapping requestMapping=method.getAnnotation(RequestMapping.class);
                //拼接虚拟请求资源的url路径
                String regex="/"+baseUrl+"/"+requestMapping.value();
                 //将给定的正则表达式编译并赋予给Pattern类
                Pattern pattern=Pattern.compile(regex);
                //保存当前url和method的映射关系
                //当前handler能够处理的请求,必须满足上面的正则验证才可以
                handlerMapping.add(new Handler(pattern,entry.getValue(),method));
                System.out.println("Mapped: "+regex+ " , "+method);
            }
        }
    }

    //到此为止,初始化的工作已经完成了
    //实现运行逻辑
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    //doPost()方法中用了委派模式,委派模式的具体体现在doDispatch方法中
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            doDispatch(req,resp);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
             resp.getWriter().write("500 Excetion,Detail: "+Arrays.toString(e.getStackTrace()));
        }
    }

    private void doDispatch(HttpServletRequest req,HttpServletResponse resq) throws IOException, InvocationTargetException, IllegalAccessException
    {
        Handler handler = getHandler(req);
        if(handler==null)
        {
          resq.getWriter().write("404 Not Found!!!");
         return;
        }
        //获取请求参数
        Map<String, String[]> parameterMap = req.getParameterMap();
        //获取方法形参列表的参数类型的数组
        Class<?>[] parameterTypes = handler.method.getParameterTypes();
        //保存请求参数值的数组
        Object[] paramValues=new Object[parameterTypes.length];
        //遍历请求参数的map
        //默认所有请求参数,都存在对应的形参来接收
        for(Map.Entry<String,String[]> parm:parameterMap.entrySet())
        {
            //获取当前请求参数的key对应的value数组
            String value=Arrays.toString(parm.getValue());
            //获取处理当前请求参数的对应方法上的形参的索引
            Integer index = handler.paramIndexMapping.get(parm.getKey());
            //保存--请求参数到形参,需要进行强制类型转换
            paramValues[index]=convert(parameterTypes[index],value);
        }
        //不位于请求参数之中,但用户可能也需要的
        //如果参数属性映射集合中存在request的全类名,说明形参上出现了该参数,用户需要它
        if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName()))
        {
            int reqIndex=handler.paramIndexMapping.get(HttpServletRequest.class.getName());
            paramValues[reqIndex]=req;
        }

        if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName()))
        {
            int reqIndex=handler.paramIndexMapping.get(HttpServletResponse.class.getName());
            paramValues[reqIndex]=resq;
        }
        
        //返回调用方法
        Object returnValue = handler.method.invoke(handler.controller, paramValues);
        //如果返回值为空,啥也不干,否则输出到页面
        if(returnValue==null||returnValue instanceof Void)
        {
            return;
        }
        resq.getWriter().write(returnValue.toString());
    }

    //Handler记录Controller中的RequestMapping和Mehtod的对应关系
    //内部类--->url和对应method的映射关系保存
    private  class Handler{
        protected Object controller;//保存方法对应的实例
        protected Method method;//保存映射的方法
        protected Pattern pattern;//支持正则
        protected Map<String,Integer> paramIndexMapping;//参数顺序

        protected  Handler(Pattern pattern,Object controller,Method method)
        {
            this.controller=controller;
            this.method=method;
            this.pattern=pattern;
            paramIndexMapping=new HashMap<>();
             //处理当前method方法的参数,并保存参数的属性
            putParamIndexMapping(method);
         }
        //处理并保存参数的顺序
        private void putParamIndexMapping(Method method)
        {
            //提取方法中加了注解的参数
            Annotation[][] pa = method.getParameterAnnotations();
            //第一个维度标识的是第i个参数的所有注解
            //第二个维度标识的是第i个参数的第j个注解
            for(int i=0;i<pa.length;i++)
            {
                for (Annotation a : pa[i]) {
                    if(a instanceof RequestParam)
                    {
                        String paramName = ((RequestParam) a).value();
                        if(!"".equals(paramName.trim()))
                        {
                            paramIndexMapping.put(paramName,i);
                        }
                    }
                }
            }
            //提取方法中的request和response参数
            Class<?>[] parameterTypes = method.getParameterTypes();
            int i=0;
            for (Class<?> parameterType : parameterTypes) {
                if(parameterType==HttpServletRequest.class||parameterType==HttpServletResponse.class)
                {
                    //类名的全类名
                    paramIndexMapping.put(parameterType.getName(),i);
                }
                i++;
            }
        }
    }

    private  Handler getHandler(HttpServletRequest req)
    {
        if(handlerMapping.isEmpty()){return null;}
        //URI ===> 工程名+虚拟路径 ===> /demo1/getAllUsers
        String url=req.getRequestURI();
        //工程名===>/demo1
        String contextPath = req.getContextPath();
        //只保存虚拟路径===>/getAllUsers
        url= url.replace(contextPath, "");
        for (Handler handler : handlerMapping) {
            //找到能够处理当前url请求的handler
            Matcher matcher = handler.pattern.matcher(url);
            //如果没有匹配上,继续匹配下一个
            if(!matcher.matches()){continue;}
            return handler;
        }
        return null;
    }
    
    //请求参数到形参赋值的强制类型转换
    //url传过传过来的参数都是String类型的,由于HTPP基于字符串协议
    //只需要把String转换成任意类型
    private Object convert(Class<?> type,String value)
    {
        if(Integer.class==type)
        {
            return Integer.valueOf(value);
        }
        //如果还有double或者其他类型,可以采用策略模式
        //这里暂时不做实现
        return value;
    }
}

在以上代码中增加了两个方法,一个是getHandler方法,主要负责处理url和method的映射关系,以及url的正则匹配,另一个是convert方法,主要负责url参数的强制类型转换

总结

至此,手写Mini版Spring MVC框架就全部完成了

相关文章