手写 mini SpringMVC 核心代码

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写 mini SpringMVC 核心代码相关的知识,希望对你有一定的参考价值。


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

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

以上是关于手写 mini SpringMVC 核心代码的主要内容,如果未能解决你的问题,请参考以下文章

300行代码手写SpringMVC

手写一个Mybatis框架

2021年,让我们手写一个mini版本的vue2.x和vue3.x框架

自己手写一个Mybatis框架(简化)

带你手写一个SpringMVC框架(有助于理解springMVC)

springMvc原理和手写springMvc框架