SpringMVC---->自我实现底层机制(吃透springMVC)

目录

配套代码在资源中(免费)

maven环境搭配 

注解注入的规范:

一.开发HongDisptcherServlet前端控制器

1.说明:

2.配置web.xml文件

3.检查前期工作是否成功

二.完成客户端/浏览器请求控制层

1.创建 自己的 Controller 和自定义注解

2.配置自己的spring容器文件

3.编写XMLParser工具类,可以解析hongspringmvc.xml

这里来说明一下SAXReader解析xml文件

4.开发自己的spring容器(HongWebApplicationContext)

4.1把指定的目录包括子目录下的 java 类的全路径扫描到集合中,比如 ArrayList 

4.2扫描service中的类

5.实例化对象到容器中

将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service...), 反射注

入到 ioc 容器

6.完成请求URL和控制器方法的映射关系(HongHandler)

完成

7.完成HongDispatcherServlet分发请求到对应控制器方法executeDispatch()

7.1通过request对象,返回HongHandler对象

7.2完成分发请求任务

思路:

三.从 web.xml 动态获取 hspspringmvc.xml

四.完成自定义@Service 注解功能

如果给某个类加上@Service, 则可以将其注入到我们的 Spring 容器

1.@Service注解:用于表示一个service对象,并注入到spring容器

2.Monster.java

3.MonsterServiceImpl/对象 作为service注入到spring容器

五.完成 Spring 容器对象的自动装配 -@Autowried

完成 Spring 容器中对象的注入/自动装配

1.完成属性的自动装配

思路:

六.完成控制器方法获取参数-@RequestParam

自定义@RequestParam 和 方法参数名获取参数

1.将方法的 HttpServletRequest 和 HttpServletResponse 参数封装到参数数组,进行反射调用

2.在方法参数 指定 @RequestParam 的参数封装到参数数组,进行反射调用

注解

将http请求参数封装到params数组中, 提示,要注意填充实参的时候,顺序问题

编写方法,返回请求参数是目标方法的第几个形参

3.在方法参数 没有指定 @RequestParam ,按照默认参数名获取值, 进行反射调用

通过方法返回的 String, 转发或者重定向到指定页面

编写方法, 得到目标方法的所有形参的名称,并放入到集合中返回

七.完成简单视图解析

通过方法返回的 String, 转发或者重定向到指定页面

思路

1.登录页面

2.失败页面

3.成功页面

处理妖怪登录的方法,返回要请求转发/重定向的字符串

    这里就是对返回的结果进行解析(判断是forward 还是 redirect)

   八.完成返回 JSON 格式数据-@ResponseBody 

1.@RequestBody的基本认识

简单说就是如果你想让这个方法以json的形式返回一个结果,就可以添加这个注解

2.需求说明:

3.代码

当读取到该注解

九.最后的总结


 

配套代码在资源中(免费)

maven环境搭配 

aa69962b883c458ebae9752bf6688c22.png


<!--    引入原生servlet依赖-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
<!--      scope表示引入的jar的作用范围,-->
<!--      provided:表示该项目在打包时,放到生产环境的时候,不需要带上servlet-api.jar-->
<!--      因为tomcat本身是由servlet包的,到时直接使用tomcat本身的servlet-api.jar,防止版本冲突-->
      <scope>provided</scope>
    </dependency>
<!--    引入dom4j,解析xml文件-->
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.6.1</version>
    </dependency>
<!--    引入常用的工具jar包,该jar有很多常用的类-->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.5</version>
    </dependency>
  </dependencies>

注解注入的规范:

首先我们会先去使用isAnnotationPresen()读取路径上有么有注解,有的话我们就会继续看看这个注解后面是否有value值,有就按照这个value值去注入到ioc中,没有就进行类名或者接口名首字母小写的转化,然后依然注入到ioc中

一.开发HongDisptcherServlet前端控制器

1.说明:

HongDisptcherServlet充当springmvc原生的DisptcherServlet

hongspringmvc充当spring容器配置文件

2.配置web.xml文件

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>


<!--  配置HongDisptcherServlet-->
  <servlet>
    <servlet-name>HongDisptcherServlet</servlet-name>
    <servlet-class>com.hong.hongspringmvc.servlet.HongDisptcherServlet</servlet-class>
<!--    给HongDisptcherServlet配置参数,指定要操作的spring容器配置文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:hongspringmvc.xml</param-value>
    </init-param>
    
<!--    HongDisptcherServlet在tomcat启动时就自动加载-->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>HongDisptcherServlet</servlet-name>
<!--    因为HongDisptcherServlet作为前端控制器,所以需要拦截所以请求-->
<!--    url-pattern对外提供访问Servlet的地址就是http://ip[域名]:port/过程路径/*-->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

3.检查前期工作是否成功

public class HongDisptcherServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("调用成功");
    }

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

当启动tomcat的时候,我们随便输入一个最后的页面地址,就会发现调用成功,那么我们的第一步就先完成了。

二.完成客户端/浏览器请求控制层

b94163a9d7634d00986ccf2ad00a6a75.png        

目标:在自己的控制器这边写一个方法然后浏览器请求打过来之后,希望前端控制器能够进行分发处理到Controller的每个方法

1.创建 自己的 Controller 和自定义注解

@Controller
public class MonsterController {
    //springmvc是支持原生的servle api
    @RequestMapping("/list/monster")
    public void listMonster(HttpServletRequest request, HttpServletResponse response){
        //设置编码和返回类型
        response.setContentType("text/html;charset=utf-8");
        //获取Writer返回信息
        try {
            PrintWriter printWriter=response.getWriter();
            printWriter.write("<h1>妖怪列表信息</h1>");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

}
//注解用于标识一个控制器组件
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
public @interface Controller {
 String value() default  " ";
}
//注解用于标识一个控制器组件
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
 String value() default  " ";
}

2.配置自己的spring容器文件

<?xml version="1.0" encoding="utf-8" ?>
<beans>
<!--    指定要扫描的基本包-->
    <component-scan base-package="com.hong.controller"></component-scan>
</beans>

3.编写XMLParser工具类,可以解析hongspringmvc.xml

public class XMLParser {

    public static String getBasePackage(String xmlFile) {

        //这个解析的过程,是前面讲过的
        SAXReader saxReader = new SAXReader();
        //通过得到类的加载路径-》获取到spring配置文件.[对应的资源流]
        InputStream inputStream =
                XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);
        try {
            //得到xmlFile文档
            Document document = saxReader.read(inputStream);
            Element rootElement = document.getRootElement();
            Element componentScanElement =
                    rootElement.element("component-scan");
            Attribute attribute = componentScanElement.attribute("base-package");
            String basePackage = attribute.getText();
            return basePackage;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

这里来说明一下SAXReader解析xml文件

读取配置文件步骤如下:
1.首先实例化SAXReader对象,然后用Document对象获取配置文件的数据。    

  //实例化对象
        SAXReader read=new SAXReader();
        //获取配置文件的数据
        Document doc=read.read("src/web.xml");

2.获取根节点元素:  

//获取根节点元素
        String rootElement=doc.getRootElement().getName();
        System.out.println("根节点元素:"+rootElement);

3.获取根节点里某个属性的的属性值:                                                   

  //属性名
        String rootName=doc.getRootElement().attributeValue("name");
        System.out.println("根节点里name属性的属性值:"+rootName);

4.开发自己的spring容器(HongWebApplicationContext)

1.得到扫描类的全路径

2.注入相关的对象(Controller,Service...)

4.1把指定的目录包括子目录下的 java 类的全路径扫描到集合中,比如 ArrayList 

当tomcat启动的时候会先去加载前端控制器,在init()方法中创建我们自己的spring容器并初始化

通过所写的自己的spring容器中的init获得前端控制器的路径

 public void scanPackage(String pack) {

        //得到包所在的工作路径[绝对路径]
        //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径]
        //比如 "com.hong.controller" => url 是 D:\hong_springmvc\hong-springmvc\target\hong-springmvc\WEB-INF\classes\com\hong\controller
        //细节说明: 1. 不要直接使用Junit测试, 否则 url null
        //             2. 启动tomcat来去测试(因为会调用前端控制器来初始化spring容器)
        URL url =
                this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));

        //System.out.println("url=" + url);
        //根据得到的路径, 对其进行扫描,把类的全路径,保存到classFullPathList

        String path = url.getFile();
        System.out.println("path= " + path);
        //在io中,把目录,视为一个文件
        File dir = new File(path);
        //遍历dir[文件/子目录]
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {//如果是一个目录,需要递归扫描
                scanPackage(pack + "." + f.getName());
            } else {
                //说明:这时,你扫描到的文件,可能是.class, 也可能是其它文件
                //就算是.class, 也存在是不是需要注入到容器
                //目前先把文件的全路径都保存到集合,后面在注入对象到容器时,再处理
                String classFullPath =
                        pack + "." + f.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }

4.2扫描service中的类

//编写方法,完成自己的spring容器的初始化
    public void init() {
        //这里是写的固定的spring容器配置文件.?=>做活
        String basePackage = XMLParser.getBasePackage("hongspringmvc.xml");
        //这时basePackage => com.hong.controller,com.hong.service
        String[] basePackages = basePackage.split(",");
        //遍历basePackages, 进行扫描
        if (basePackages.length > 0) {
            for (String pack : basePackages) {
                scanPackage(pack);//扫描
            }
        }
        System.out.println("扫描后的= classFullPathList=" + classFullPathList);
        //将扫描到的类, 反射到ico容器
        executeInstance();
        System.out.println("扫描后的 ioc容器= " + ioc);

        //完成注入的bean对象,的属性的装配
        executeAutoWired();
        System.out.println("装配后 ioc容器= " + ioc);
    }

5.实例化对象到容器中

将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service...), 反射注

入到 ioc 容器

//定义属性ioc, 存放反射生成的Bean对象 /Controller/Service
    public ConcurrentHashMap<String, Object> ioc =
            new ConcurrentHashMap<>();

编写方法,将扫描到的类, 在满足条件的情况下,反射到ioc容器 

//编写方法,将扫描到的类, 在满足条件的情况下,反射到ioc容器
    public void executeInstance() {
        //判断是否扫描到类
        //classFullPathList前面所写的类的集合
        if (classFullPathList.size() == 0) {//说明没有扫描到类
            return;
        }
        try {
            //遍历classFullPathList,进行反射
            for (String classFullPath : classFullPathList) {
                Class<?> clazz = Class.forName(classFullPath);
                //说明当前这个类有@Controller
                if (clazz.isAnnotationPresent(Controller.class)) {
                    //得到类名首字母小写
                    String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() +
                            clazz.getSimpleName().substring(1);
                    ioc.put(beanName, clazz.newInstance());
                } //如果有其它的注解,可以扩展 ,

6.完成请求URL和控制器方法的映射关系(HongHandler)

将配置的 @RequestMapping url 和 对应的 控制器 - 方法 映射关系保存到集合中

 

**
 * Created with IntelliJ IDEA.
 *
 * @Author: 海绵hong
 * @Date: 2022/10/29/16:13
 * @Description:对象记录请求的 url 和 控制器方法映射关系
 */
public class HongHandler {
    private String url;
    private Object controller;
    private Method method;

    public HongHandler() {
    }

    public HongHandler(String url, Object controller, Method method) {
        this.url = url;
        this.controller = controller;
        this.method = method;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    @Override
    public String toString() {
        return "HongHandler{" +
                "url='" + url + '\'' +
                ", controller=" + controller +
                ", method=" + method +
                '}';
    }
}
/定义属性 handlerList , 保存hongHandler[url和控制器方法的映射]
    private List<HongHandler> handlerList =
            new ArrayList<>();
编写方法,完成url 和 控制器方法的映射
//编写方法,完成url 和 控制器方法的映射
    private void initHandlerMapping() {
        if (hongWebApplicationContext.ioc.isEmpty()) {
            //判断当前的ioc容器是否为null
            return;
        }

        //遍历ioc容器的bean对象,然后进行url映射处理
        //java基础 map的遍历
        for (Map.Entry<String, Object> entry : hongWebApplicationContext.ioc.entrySet()) {
            //先取出注入的Object的class对象
            Class<?> clazz = entry.getValue().getClass();
            //如果注入的Bean是Controller
            if (clazz.isAnnotationPresent(Controller.class)) {
                //取出它的所有方法
                Method[] declaredMethods = clazz.getDeclaredMethods();
                //遍历方法
                for (Method declaredMethod : declaredMethods) {
                    //判断该方法是否有@RequestMapping
                    if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
                        //取出@RequestMapping值->就是映射路径
                        RequestMapping requestMappingAnnotation =
                                declaredMethod.getAnnotation(RequestMapping.class);
                        //这里小伙伴可以把工程路径+url
                        //getServletContext().getContextPath()
                        // /springmvc/monster/list
                        String url = requestMappingAnnotation.value();
                        //创建hongHandler对象->就是一个映射关系
                        HongHandler hongHandler = new HongHandler(url, entry.getValue(), declaredMethod);
                        //放入到handlerList
                        handlerList.add(hongHandler);
                    }
                }
            }
        }
    }

完成

根据initHandlerMapping()方法的处理,我们就可以将当前注入到ioc容器的bean对象(主要是Controller),把它的映射关系,已经放到了一个hongHandler(对象)中,并将hongHandler放到了一个集合中

7.完成HongDispatcherServlet分发请求到对应控制器方法executeDispatch()

当一个请求打过来之后先走到前端控制器,因为前端控制器的doget和doport方法是要拦截你所以请求的,在这个请求里面我是不是可以拿到这个url,然后到映射关系的集合initHandlerMapping()里面去找,这个url有没有,如果有我就去调这个executeDispatch()进行分发,拿不到说明你的地址是由问题的,返回一个404就欧克了。

7.1通过request对象,返回HongHandler对象

//编写方法,通过request对象,返回HongHandler对象
    //如果没有,就返回null
    private HongHandler getHongHandler(HttpServletRequest request) {
        //1.先获取的用户请求的uri 比如http://localhost:8080/springmvc/monster/list
        //  uri = /springmvc/monster/list
        //2. 这里小伙伴要注意得到uri 和 保存url 是有一个工程路径的问题
        // 两个方案解决 =>第一个方案: 简单 tomcat 直接配置 application context =>/
        // 第二个方案 保存 honghandler对象 url 拼接 getServletContext().getContextPath()
        String requestURI = request.getRequestURI();
        //遍历handlerList
        for (HongHandler hongHandler : handlerList) {
            if (requestURI.equals(hongHandler.getUrl())) {//说明匹配成功
                return hongHandler;
            }
        }
        return null;
    }

7.2完成分发请求任务

思路:

当我们调用这个方法的时候先去判断用户请求的路径是否存在,不存在就返回404,存在就开始在之前的hongHandler集合(ioc容器的bean对象(主要是Controller)的数据存入到了hongHandler的集合中hongHandler类型的集合)里面,这个时候executeDispatch()就会去分发找到对应的url。

通过调用invoke方法来执行对象的某个方法

 private void executeDispatch(HttpServletRequest request,
                                 HttpServletResponse response) {

        HongHandler hongHandler = getHongHandler(request);
        try {
            if (null == hongHandler) {//说明用户请求的路径/资源不存在
                response.getWriter().print("<h1>404 NOT FOUND</h1>");
            } else {//匹配成功, 反射调用控制器的方法
                HongHandler.getMethod()
                      .invoke(hongHandler.getController(),request,response);
            } catch (Exception e) {
            e.printStackTrace();
        }
    }

由executeDispatch()去找handler,然后反射到自己的处理器

三.从 web.xml 动态获取 hspspringmvc.xml

前面我们加载 hongspringmvc.xml 是硬编码, 现在做活, 从 web.xml

 

//获取到web.xml中的 contextConfigLocation
        /*
         <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:hongspringmvc.xml</param-value>
        </init-param>
         */
        String configLocation =
                servletConfig.getInitParameter("contextConfigLocation");//拿到的是classpath:hongspringmvc.xml
//这里是写的固定的spring容器配置文件.?=>做活
        //String basePackage = XMLParser.getBasePackage("hongspringmvc.xml");
        String basePackage =
                XMLParser.getBasePackage(configLocation.split(":")[1]);

四.完成自定义@Service 注解功能

如果给某个类加上@Service, 则可以将其注入到我们的 Spring 容器

4ccd4146151845b18215a90ca207e6b8.png

- Service 类标注 @Service, 可以将对象注入到 Spring 容器中
- 并可以 通过接口名 支持多级 , 类名来获取到 Service Bean
 

1.@Service注解:用于表示一个service对象,并注入到spring容器

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

2.Monster.java

public class Monster {
    private Integer id;
    private String name;
    private String skill;
    private Integer age;

    public Monster(Integer id, String name, String skill, Integer age) {
        this.id = id;
        this.name = name;
        this.skill = skill;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Monster{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", skill='" + skill + '\'' +
                ", age=" + age +
                '}';
    }
}

3.MonsterServiceImpl/对象 作为service注入到spring容器

public interface MonsterService{

    //增加方法-返回monster列表
    public List<Monster> listMonster();

    //增加方法,通过传入的name,返回monster列表
    public List<Monster> findMonsterByName(String name);
}
@Service
public class MonsterServiceImpl implements MonsterService {
    @Override
    public List<Monster> listMonster() {
        //这里就模拟数据->DB
        List<Monster> monsters =
                new ArrayList<>();
        monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
        monsters.add(new Monster(200, "老猫妖怪", "抓老鼠", 200));
        return monsters;
    }


}

现在在Service中也添加了数据,但是我们只是将@Service注解读取了,没有注入到ioc中,所以接下来我们需要将@service注解读入到ioc中。而之前我们的@Controller注解之后我们就可以添加另外一种情况了(就是读取到这个注解)

  else if (clazz.isAnnotationPresent(Service.class)) {//如果类有@Serivce注解

                    //先获取到Service的value值=> 就是注入时的beanName
                    Service serviceAnnotation =
                            clazz.getAnnotation(Service.class);

                    String beanName = serviceAnnotation.value();
                    if ("".equals(beanName)) {//说明没有指定value, 我们就使用默认的机制注入Service
                        //可以通过接口名/类名[首字母小写]来注入ioc容器
                        //1.得到所有接口的名称=>反射
                        Class<?>[] interfaces = clazz.getInterfaces();

                        Object instance = clazz.newInstance();
                        //2. 遍历接口,然后通过多个接口名来注入
                        for (Class<?> anInterface : interfaces) {
                            //接口名->首字母小写
                            String beanName2 = anInterface.getSimpleName().substring(0, 1).toLowerCase() +
                                    anInterface.getSimpleName().substring(1);
                            ioc.put(beanName2, instance);
                        }

                    } else {//如果有指定名称,就使用该名称注入即可
                        ioc.put(beanName, clazz.newInstance());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

五.完成 Spring 容器对象的自动装配 -@Autowried

完成 Spring 容器中对象的注入/自动装配

spring会默认优先根据(被注解修饰的)属性类型去容器中找对应的组件(bean),找到就赋值;若找到多个相同类型的组件,再将属性的名称作为组件(bean)的id去容器中查找。

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

1.完成属性的自动装配

 public void executeAutoWired() {
        //判断ioc有没有要装配的对象
        if (ioc.isEmpty()) {
            return; //你也可以抛出异常 throw new RuntimeException("ioc 容器没有bean对象")
        }
        //遍历ioc容器中的所有注入的bean对象, 然后获取到bean的所有字段/属性,判断是否需要
        //装配
        /**
         * entry => <String,Object > String 就是你注入对象时名称 Object就是bean对象
         */
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {

            //String key = entry.getKey();
            Object bean = entry.getValue();

            //得到bean的所有字段/属性
            Field[] declaredFields = bean.getClass().getDeclaredFields();
            for (Field declaredField : declaredFields) {
                //判断当前这个字段,是否有@AutoWired
                if (declaredField.isAnnotationPresent(AutoWired.class)) {//有@AutoWired

                    //的当前这个字段的@AutoWired
                    AutoWired autoWiredAnnotation = declaredField.getAnnotation(AutoWired.class);
                    String beanName = autoWiredAnnotation.value();
                    if ("".equals(beanName)) {//如果没有设置value,按照默认规则
                        //即得到字段类型的名称的首字母小写,作为名字来进行装配
                        Class<?> type = declaredField.getType();
                        beanName = type.getSimpleName().substring(0, 1).toLowerCase() +
                                type.getSimpleName().substring(1);
                    }
                    //如果设置value, 直接按照beanName来进行装配
                    //从ioc容器中获取到bean
                    if (null == ioc.get(beanName)) {//说明你指定的名字对应的bean不在ioc容器
                        throw new RuntimeException("ioc容器中, 不存在你要装配的bean");
                    }
                    //防止属性是private, 我们需要暴力破解
                    declaredField.setAccessible(true);
                    //可以装配属性
                    try {
                        declaredField.set(bean, ioc.get(beanName));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }

        }

    }

自动装配的特性:如待装配的类型对应的bean在IOC容器中有多个,则使用待装配的属性的属性名作为id值再进行查找,找到就装配,找不到就抛异常

思路:

1.就是先遍历ioc容器中的所有注入的bean对象, 然后获取到bean的所有字段/属性,判断是否需要,

2.判断当前这个字段,是否有@AutoWired

3.有则:如果没有设置value,按照默认规则:即得到字段类型的名称的首字母小写,作为名字来进行装配;

        如果设置value, 直接按照beanName来进行装配

4.装配:

declaredField.setAccessible(true);
                    //可以装配属性

六.完成控制器方法获取参数-@RequestParam

自定义@RequestParam 和 方法参数名获取参数

@RequestParam (value = "name" )就是为了获取到浏览器地址中的属性名(名字要相同),并装配到方法的参数中

 

在浏览器提交了一个请求打到了前端控制器支撑对这个注解的一个解析,然后在前端控制器完成一个映射

难点:将浏览器请求来的参数进行处理,考虑目标方法形参是多种形式问题

1.将方法的 HttpServletRequest HttpServletResponse 参数封装到参数数组,进行反射调用

//目标将: HttpServletRequest 和 HttpServletResponse封装到参数数组
                //1. 得到目标方法的所有形参参数信息[对应的数组]
                Class<?>[] parameterTypes =
                        hongHandler.getMethod().getParameterTypes();

                //2. 创建一个参数数组[对应实参数组], 在后面反射调用目标方法时,会使用到
                Object[] params =
                        new Object[parameterTypes.length];

                //3遍历parameterTypes形参数组,根据形参数组信息,将实参填充到实参数组

                for (int i = 0; i < parameterTypes.length; i++) {
                    //取出每一个形参类型
                    Class<?> parameterType = parameterTypes[i];
                    //如果这个形参是HttpServletRequest, 将request填充到params
                    //在原生SpringMVC中,是按照类型来进行匹配,这里简化使用名字来进行匹配
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                        params[i] = request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i] = response;
                    }
                }

2.在方法参数 指定 @RequestParam 的参数封装到参数数组,进行反射调用

    @Override
    public List<Monster> findMonsterByName(String name) {
        //这里老师就模拟数据->DB
        List<Monster> monsters =
                new ArrayList<>();
        monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
        monsters.add(new Monster(200, "老猫妖怪", "抓老鼠", 200));
        monsters.add(new Monster(300, "大象精", "运木头", 100));
        monsters.add(new Monster(400, "黄袍怪", "吐烟雾", 300));
        monsters.add(new Monster(500, "白骨精", "美人计", 800));


        //创建集合返回查询到的monster集合

        List<Monster> findMonsters =
                new ArrayList<>();
        //遍历monsters,返回满足条件
        for (Monster monster : monsters) {
            if (monster.getName().contains(name)) {
                findMonsters.add(monster);
            }
        }
        return findMonsters;
    }

    @Override
    public boolean login(String name) {
        //实际上会到DB验证->这里模拟
        if ("白骨精".equals(name)) {
            return true;
        } else {
            return false;
        }

注解

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

将http请求参数封装到params数组中, 提示,要注意填充实参的时候,顺序问题

//将http请求参数封装到params数组中, 提示,要注意填充实参的时候,顺序问题

                //1. 获取http请求的参数集合
                //解读
                //http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
                //2. 返回的Map<String,String[]> String:表示http请求的参数名
                //   String[]:表示http请求的参数值,为什么是数组
                //
                //处理提交的数据中文乱码
                request.setCharacterEncoding("utf-8");
                Map<String, String[]> parameterMap =
                        request.getParameterMap();

                //2. 遍历parameterMap 将请求参数,按照顺序填充到实参数组params
                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {

                    //取出key,这name就是对应请求的参数名
                    String name = entry.getKey();
                    //说明:这里只考虑提交的参数是单值的情况,即不考虑类似checkbox提示的数据
                    //    这里做了简化,如果小伙伴考虑多值情况,也不难..
                    String value = entry.getValue()[0];
                    //我们得到请求的参数对应目标方法的第几个形参,然后将其填充
                    //这里专门编写一个方法,得到请求的参数对应的是第几个形参
                    //1. API 2. java内力真正增加..3.忠告
                    int indexRequestParameterIndex =
                            getIndexRequestParameterIndex(hongHandler.getMethod(), name);
                    if (indexRequestParameterIndex != -1) {//找到对应的位置
                        params[indexRequestParameterIndex] = value;
                    } else {//说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置[待..]

                        //思路
                        //1. 得到目标方法的所有形参的名称-专门编写方法获取形参名
                        //2. 对得到目标方法的所有形参名进行遍历,如果匹配就把当前请求的参数值,填充到params
                        List<String> parameterNames =
                                getParameterNames(hongHandler.getMethod());
                        for (int i = 0; i < parameterNames.size(); i++) {
                            //如果请求参数名和目标方法的形参名一样,说明匹配成功
                            if (name.equals(parameterNames.get(i))) {
                                params[i] = value;//填充到实参数组
                                break;
                            }
                        }
                    }
                }

编写方法,返回请求参数是目标方法的第几个形参

  //编写方法,返回请求参数是目标方法的第几个形参

    /**
     * @param method 目标方法
     * @param name   请求的参数名
     * @return 是目标方法的第几个形参
     */
    public int getIndexRequestParameterIndex(Method method, String name) {

        //1.得到method的所有形参参数
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //取出当前的形参参数
            Parameter parameter = parameters[i];
            //判断parameter是不是有@RequestParam注解
            boolean annotationPresent = parameter.isAnnotationPresent(RequestParam.class);
            if (annotationPresent) {//说明有@RequestParam
                //取出当前这个参数的 @RequestParam(value = "xxx")
                RequestParam requestParamAnnotation =
                        parameter.getAnnotation(RequestParam.class);
                String value = requestParamAnnotation.value();
                //这里就是匹配的比较
                if (name.equals(value)) {
                    return i;//找到请求的参数,对应的目标方法的形参的位置
                }
            }
        }
        //如果没有匹配成功,就返回-1
        return -1;
    }

3.在方法参数 没有指定 @RequestParam ,按照默认参数名获取值, 进行反射调用

通过方法返回的 String, 转发或者重定向到指定页面

编写方法, 得到目标方法的所有形参的名称,并放入到集合中返回

 //编写方法, 得到目标方法的所有形参的名称,并放入到集合中返回

    /**
     * @param method 目标方法
     * @return 所有形参的名称, 并放入到集合中返回
     */
    public List<String> getParameterNames(Method method) {

        List<String> parametersList = new ArrayList<>();
        //获取到所以的参数名->这里有一个小细节
        //在默认情况下 parameter.getName() 得到的名字不是形参真正名字
        //而是 [arg0, arg1, arg2...], 这里我们要引入一个插件,使用java8特性,这样才能解决
        Parameter[] parameters = method.getParameters();
        //遍历parameters 取出名称,放入parametersList
        for (Parameter parameter : parameters) {
            parametersList.add(parameter.getName());
        }
        System.out.println("目标方法的形参列表=" + parametersList);
        return parametersList;
    }

引入插件加入pom.xml中

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.7.0</version>
    <configuration>
      <source>1.8</source>
      <target>1.8</target>
      <compilerArgs>
        <arg>-parameters</arg>
      </compilerArgs>
      <encoding>utf-8</encoding>
    </configuration>
  </plugin>
{//说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置[待..]

                        //思路
                        //1. 得到目标方法的所有形参的名称-专门编写方法获取形参名
                        //2. 对得到目标方法的所有形参名进行遍历,如果匹配就把当前请求的参数值,填充到params
                        List<String> parameterNames =
                                getParameterNames(hongHandler.getMethod());
                        for (int i = 0; i < parameterNames.size(); i++) {
                            //如果请求参数名和目标方法的形参名一样,说明匹配成功
                            if (name.equals(parameterNames.get(i))) {
                                params[i] = value;//填充到实参数组
                                break;
                            }
                        }
                    }

七.完成简单视图解析

通过方法返回的 String, 转发或者重定向到指定页面

完成任务说明
- 用户输入 白骨精 , 可以登录成功 , 否则失败
- 根据登录的结果 , 可以重定向或者请求转发到 login_ok.jsp / login_error.jsp, 并显示妖

 

思路

在handler中会增加一个目标方法,专门处理登录,返回一个结果(String),进行请求转发还是重定向返回给前端控制器。然后前端处理器会去调用视图解析器,同时视图解析器也会返回一个结果。视图解析器对handler返回的结果进行解析,这个时候有两种结果:1.请求转发,2,重定向

再由视图解析器进行解析

1.登录页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
<form action="/monster/login" method="post">
    妖怪名: <input type="text" name="mName"><br/>
    <input type="submit" value="登录">
</form>
</body>
</html>

2.失败页面

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录失败</title>
</head>
<body>
<h1>登录失败</h1>
sorry, 登录失败 ${requestScope.mName}
</body>
</html>

3.成功页面

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
欢迎你: ${requestScope.mName}
</body>
</html>

处理妖怪登录的方法,返回要请求转发/重定向的字符串

//处理妖怪登录的方法,返回要请求转发/重定向的字符串
    @RequestMapping("/monster/login")
    public String login(HttpServletRequest request,
                        HttpServletResponse response,
                        String mName) {

        System.out.println("--接收到mName---" + mName);
        //将mName设置到request域
        request.setAttribute("mName", mName);
        boolean b = monsterService.login(mName);
        if (b) {//登录成功!
            //return "forward:/login_ok.jsp";
            //测试重定向
            //return "redirect:/login_ok.jsp";
            //测试默认的方式-forward
            return "/login_ok.jsp";

        } else {//登录失败
            return "forward:/login_error.jsp";
        }
    }

    这里就是对返回的结果进行解析(判断是forward 还是 redirect)

 //这里就是对返回的结果进行解析=>原生springmvc 可以通过视图解析器来完成
                //这里直接解析,只要把视图解析的核心机制讲清楚就OK
                if (result instanceof String) {

                    String viewName = (String) result;
                    if(viewName.contains(":")){//说明你返回的String 结果forward:/login_ok.jsp 或者 redirect:/xxx/xx/xx.xx
                        String viewType = viewName.split(":")[0];//forward | redirect
                        String viewPage = viewName.split(":")[1];//是你要跳转的页面名
                        //判断是forward 还是 redirect
                        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);
                    }

                }

   八.完成返回 JSON 格式数据-@ResponseBody 

1.@RequestBody的基本认识

        @RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。

        如果后端参数是一个对象,且该参数前是以@RequestBody修饰的,那么前端传递json参数时,必须满足以下要求:

        后端@RequestBody注解对应的类在将HTTP的输入流(含请求体)装配到目标类(即:@RequestBody后面的类)时,会根据json字符串中的key来匹配对应实体类的属性,如果匹配一致且json中的该key对应的值符合(或可转换为),这一条我会在下面详细分析,其他的都可简单略过,但是本文末的核心逻辑代码以及几个结论一定要看! 实体类的对应属性的类型要求时,会调用实体类的setter方法将值赋给该属性。

        json字符串中,如果value为""的话,后端对应属性如果是String类型的,那么接受到的就是"",如果是后端属性的类型是Integer、Double等类型,那么接收到的就是null。

        json字符串中,如果value为null的话,后端对应收到的就是null。

        如果某个参数没有value的话,在传json字符串给后端时,要么干脆就不把该字段写到json字符串中;要么写value时, 必须有值,null  或""都行。千万不能有类似"stature":,这样的写法,

简单说就是如果你想让这个方法以json的形式返回一个结果,就可以添加这个注解

2.需求说明:

1.在实际开发中,比如前后端分离的项目,通常是直接返回json数据给客户端/浏览器

2.客户端/浏览器接收到数据后,直接决定如何处理和显示

3.代码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";
}
/**
     * 编写方法,返回json格式的数据
     * 1. 梳理
     * 2. 目标方法返回的结果是给springmvc底层通过反射调用的位置
     * 3. 我们在springmvc底层反射调用的位置,接收到结果并解析即可
     * 4. 方法上标注了 @ResponseBody 表示希望以json格式返回给客户端/浏览器
     * 5. 目标方法的实参,在springmvc底层通过封装好的参数数组,传入..
     * @param request
     * @param response
     * @return
     */

    @RequestMapping("/monster/list/json")
    @ResponseBody
    public List<Monster> listMonsterByJson(HttpServletRequest request,
                                           HttpServletResponse response) {

        List<Monster> monsters = monsterService.listMonster();
        return monsters;
    }

当读取到该注解

else if(result instanceof ArrayList) {//如果是ArrayList

                    //判断目标方法是否有@ResponseBody
                    Method method = hongHandler.getMethod();
                    if(method.isAnnotationPresent(ResponseBody.class)) {
                        //把result [ArrayList] 转成json格式数据-》返回
                        //这里我们需要使用到java中如何将 ArrayList 转成 json(writeValueAsString())
                        //这里我们需要使用jackson包下的工具类可以轻松的搞定.
                            
                        ObjectMapper objectMapper = new ObjectMapper();
                        String resultJson =
                                objectMapper.writeValueAsString(result);

                        response.setContentType("text/html;charset=utf-8");
                        //这里简单的处理,就直接返回
                        PrintWriter writer = response.getWriter();
                        writer.write(resultJson);
                        writer.flush();
                        writer.close();

                    }

九.最后的总结

首先我们先开发的是前端控制器(HongDispatherServlet)

1.先将前端控制器配置到了web.xml文件中(其实是由tomcat来创建的)

2.在前端控制器里面创建了spring容器(init方法)并初始化(初始化的过程就是将我们control和service注入到容器)

3.而这个spring容器我们专门开发了一个HongWebApplicationContext,这个类里面我们进行了扫描,进行了注入

4.init方法我们拿到spring配置文件的包,然后进行一个扫描,拿到(注解)全部路径,然后在反射到ioc容器里面,还有就是完成bean对象属性的装配工作executeAutoWired();所以这个方法将bean对象注入到了ioc中并将依赖关系装配

5.接下来在前端控制器中的initHandlerMapping---完成映射处理,将映射对象保存到集合---遍历ioc容器的bean对象,然后进行url映射处理,将这个遍历对象保存到了handler集合中了

6.接下来就是最头疼的分发请求了需要对浏览器请求来的参数进行处理,考虑目标方法形参是多种形式问题---将其封装到参数数组,以反射调用的形式传递给目标方法

                这个方法具体做的:先判断用户请求的路径和资源是否存在。接着将将http请求参数封装到params数组中调用的时候传的就是这个实参数组。这就是为啥HttpServletRequest req, HttpServletResponse resp这两个形参是如何得到实参的。就是把你传过来的参数进行封装,扫描你这个目标方法有那些参数,然后一个一个给你填进去的。

7.当你将整个目标方法调用之后打到某个目标方法之后,就会返回一个结果,这个结果就会被适配器(代码中没有实现,但是必须知道有)去调用我们自己的处理器返回一个结果。

8.根据这个结果进行视图解析(视图解析器),就会去判断如果你是一个String需要请求转发或者重定向,如果你是一个ArrayList并且有@ResponseBody这个注解,就会返回json文件

10.部分分析示意图,不想画了

92d628fa070641b4a55e551213c0bac1.png

 

 

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵hong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值