SpringMVC笔记

目录

1. SpringMVC概述

1.1 SpringMVC是什么?

基于spring框架,spring框架的一个模块,专门负责简化web开发,可以理解为servlet的一个升级。所有的web开发都是基于servlet,框架就是在其基础上加入一些功能,让开发更方便。

同spring一样,springmvc也是一个容器,用@Controller来创建、管理控制器对象。这个对象可以接收参数,显示处理结果,在功能表现上与servlet一样,但是要注意的是,这样的一个对象实质上不是servlet,只是一个普通类的对象,只是springmvc赋予了控制器对象一些额外的功能。

其中M代表Model(数据),V代表View(视图),C代表Controller(控制器类)。

实质上,就像上面说的,所有web开发的底层都是servlet,springmvc中有一个对象是servlet:DispatcherServlet(中央调度器,也称作前端控制器Front controller),它本质上其实是一个容器提供的写好的servlet,其负责接收用户的所有请求,之后转发给Controller对象(也称后端控制器Back controller),最后是Controller对象处理请求。

1.2 学习SpringMVC有什么用?

1.3 SpringMVC的处理流程

在这里插入图片描述
即SpringMVC接收请求,到处理完成的过程(背诵):

  1. 用户发起请求some.do
  2. 中央调度器接收请求,将请求转发给处理器映射器
    • 处理器映射器:实质上是SpringMVC框架的一种对象,框架把实现了HandlerMapping接口的类都称作映射器(可以有多个)
    • 作用:根据请求,从SpringMVC容器对象中获取处理器对象(类似getBean()的过程),将其放到一个叫做处理器执行链(HandlerExecutionChain)的类中保存,这个类中还以List形式保存着项目中所有的拦截器。之后,将执行链返回中央调度器。
      得到执行链的方法:
      HandlerExecutionChain mappedHandler = getHandler(processedRequest);
  3. 中央调度器将执行链交给处理器适配器(可以有多个)
    • 处理器适配器:SpringMVC框架的一种对象,实现了HandlerAdapter接口
    • 作用:执行处理器方法。
      调用适配器的方法:
      HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
      执行处理器方法:
      mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  4. 中央调度器拿到结果后将其交给视图解析器解析(可以有多个)
    • 视图解析器:SpringMVC的一种对象,实现了ViewResolver接口
    • 作用:加上配置的前后缀,组成视图完整路径,并创建一个View对象。View是表示视图的接口,在框架中,并非用字符串(a.jsp, a.html等)表示视图,而是使用VIew和它的实现类。在其中,InternalResourceView就是用来表示jsp的视图类,视图解析器在需要的时候会创建它的对象,对象中有特定的属性url来记录完整路径:/WEB-INF/view/show.jsp
  5. 中央调度器将解析结果(View对象)拿到,调用其自己的方法,将Model数据放入到request作用域,然后执行视图的forward方法,返回请求结果。

2. SpringMVC注解式开发

示例项目:

2.1 创建web项目

2.1.1 填充目录结构

2.2.2 更改web.xml版本为最新

2.2 加依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>01-HelloSpringMVC</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

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

    <!--SpringMVC依赖,包含了spring要用到的依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
  </dependencies>

  <build>

  </build>
</project>

2.3 在web.xml中注册DispatcherServlet

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--
    注册SpringMVC的核心对象DispatcherServlet,该对象在被创建的过程中会同时创建SpringMVC容器
    对象,去读取SpringMVC配置文件,把配置文件中的对象都创建好并放入到ServletContext中,当用户
    发起请求时就可以直接用那些对象了,所以我们希望在tomcat服务器启动后做的第一件事就是
    创建DispatcherServlet对象
    -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <!--
        在<servlet>标签中使用<load-on-startup>标签来表明该servlet被创建的优先级,
        一般为正整数,数字越小,优先级越高
        -->
        <load-on-startup>1</load-on-startup>

        <!--
        中央调度器创建后,会读取spring的配置文件,但是默认的读取路径是在WEB-INF下的,但是
        我们的配置文件一般都放在专门的resources下,所以我们需要修改默认的读取路径,
        在<servlet>中使用<init-param>标签修改
        -->
        <init-param>
            <!--指定更改位置的文件的类型:配置文件-->
            <param-name>contextConfigLocation</param-name>
            <!--指定被更改位置的文件的位置-->
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!--
        使用*.do则表示所有的以.do结尾的请求都交给<servlet-name>所标识的servlet
        使用如/reg这样具体的标识,则表示只有明确表示请求地址是/reg的请求会交给该servlet
        使用/:
            如果单纯使用/,则该servlet会处理所有的请求,这样我们就不用重复写xxx.do了,但是
            它同时会替代掉tomcat默认的servlet,来处理静态文件和所有未被映射到的请求,
            如果该servlet没有处理静态资源(html, js, 图片, css)的能力,则这些访问都会404,
            解决办法是在springmvc配置文件中加入能添加相应功能的标签
        -->
<!--        <url-pattern>*.do</url-pattern>-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

2.3.1 url-pattern设为/,静态资源无法访问的问题

2.3.1.1 解决方案1

在springmvc配置文件中配置:<mvc:default-servlet-handler/>

    <!--
    <mvc:default-servlet-handler/>与@RequestMapping有冲突,所以
    声明了<mvc:default-servlet-handler/>的情况下,必须声明注解驱动,将.do请求交由
    自定义业务方法处理
    -->
    <mvc:annotation-driven/>

    <!--
    在url-pattern设为/的时候,声明如下标签,增加一个处理器对象DefaultHttpRequestHandler,
    使得静态资源的访问交由该对象转发给tomcat自带的默认的servlet处理,此方式需要依赖服务器
    -->
    <mvc:default-servlet-handler/>
2.3.1.2 解决方案2

在springmvc配置文件中配置:<mvc:resources/>

    <!--
    声明如下标签,增加一个处理器对象ResourceHttpRequestHandler,将静态资源的访问交给它处理,
    不依赖服务器,
    mapping:访问静态资源的uri地址,如/images/p1.jpg,使用目录+通配符**的方式指定
    location:静态资源在项目中的位置,指定具体文件的上一级目录即可,如/images/
    -->
    <mvc:resources mapping="/images/**" location="/images/"/>
    <mvc:resources mapping="/html/**" location="/html/"/>
    <mvc:resources mapping="/js/**" location="/js/"/>
    
    <mvc:resources mapping="/static/**" location="/static/"/>

同样的,需要加入注解驱动,支持动态请求的处理。

可以将所有的静态资源均放入到同一个目录static下,这样就不用大量的写<mvc:resources/>

2.4 创建控制器类

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {
    /*
    使用springmvc框架后我们就可以使用自定义的方法来处理请求
    方法自定义,有多种返回值,常用的是ModelAndView
    有多种参数
    方法名称自定义
     */

    /*
    需要在方法名上加入@RequestMapping注解来表示此方法是控制器方法(处理器方法)
        作用:请求映射,将一个请求地址与一个控制器方法绑定在一起,一个请求指定一个方法处理。
        属性:value,是一个String数组,值为请求地址的uri,唯一值,需要加"/"
             method,控制请求地址允许的访问方式,如只允许get方式,则设值为RequestMethod.GET,
             post同理
        位置:在方法名上(常用),在类名上

    返回值:ModelAndView
        Model:数据,表示请求处理完成后,要展现给用户的数据
        View:视图,如jsp等
     */
    @RequestMapping(value = "/some.do")
    public ModelAndView doSome() {
        ModelAndView mv = new ModelAndView();
        //框架会在最后将加入的这些值放入到request作用域中
        mv.addObject("msg", "service处理后的结果");

        //指定视图,就是指定展现结果的jsp名称的完整路径,框架会对视图执行forward操作
        mv.setViewName("show.jsp");

        return mv;
    }
}

2.5 创建发起请求的页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <a href="some.do">请求</a>
</body>
</html>

2.6 创建显示结果的jsp页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <p>${msg}</p>
</body>
</html>

2.7 创建SpringMVC的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--
    如果使用注解开发,使用了创建对象的注解,如@Controller,
    则需要声明组件扫描器,来告诉框架
    -->
    <context:component-scan base-package="controller"/>
</beans>

2.8 总结

流程可以总结为:

请求页面发起some.do请求

→Tomcat服务器收到请求,通过web.xml知道需要转发给中央调度器

→中央调度器通过SpringMVC配置文件知道处理该请求的类的位置,并根据注解转发给相应方法

→框架通过执行相应方法,把得到的ModelAndView结果进行处理,转发给显示结果的jsp

2.9 定义请求规则

2.9.1 资源文件的访问限制

在上述示例中,我们创建的jsp文件都放在了webapp根目录下,这样会导致在地址栏输入资源名可以跳过登录直接访问资源的隐患,为此,除了使用在CRM项目中的“登录验证”方案外,我们还可以将jsp资源文件放到WEB-INF目录下,该目录下所有的文件都无法通过地址栏访问,而我们则可以通过更改控制器类中的路径代码的方式,在后台访问这些文件以完成前后台通信。

对于多个不同的资源文件的访问请求,我们可以通过配置视图解析器的方式,简化掉重复的路径代码。

在WEB-INF下创建新目录,将限制访问的资源放入该目录中:
在这里插入图片描述
更改控制器类中的路径代码:
mv.setViewName("/WEB-INF/view/show.jsp");

在SpringMVC配置文件中配置视图解析器:

	<!--
	声明视图解析器对象,来指定统一的路径,由框架提供,无需id。
	声明后,在控制器路径代码中,只需填写资源文件的自定义名称即可
	-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--声明前缀,即资源文件名称前的完整路径-->
        <property name="prefix" value="/WEB-INF/view/"/>
        <!--声明后缀,即资源文件名称的扩展名-->
        <property name="suffix" value=".jsp"/>
    </bean>

路径代码可简化为:
mv.setViewName("show");

2.9.2 利用@RequestMapping声明模块名称,简化路径代码

对于同一模块的不同功能的访问,会存在访问路径的重复,如activity模块,有some和other两个功能,那么请求地址为/activity/some.do和/activity/other.do,有大量重复,这时,可以使用@RequestMapping注解放在类名上的方式,简化掉重复的模块名称。

在类名上方添加注解:

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/*
@RequestMapping放在类名上:
    value:所有请求地址的公共部分,也就是模块的名称
    位置:类名的上方
 */
@Controller
@RequestMapping("/activity")
public class MyController {
    @RequestMapping(value = "/some.do")
    public ModelAndView doSome() {
        ModelAndView mv = new ModelAndView();
        mv.addObject("msg", "service处理后的结果");
        mv.setViewName("show");

        return mv;
    }
}

发起请求的页面改为:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <a href="activity/some.do">请求</a>
</body>
</html>

2.10 控制器方法的参数

在控制器类方法的形参位置可定义如下特定类型的形参,前三者自动控制框架获取相应参数,这些参数在被调用时由框架自动赋值,在方法内即可直接使用,第四类使用逐个接收或对象接收的方式获取,逐个接收与对象接收可同时在同一个控制器方法中使用,不冲突:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • 用户请求中所携带的请求参数(基本数据类型,java对象,List、Map集合或数组等)

2.10.1 直接使用

控制器类代码:

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/activity")
public class MyController {
    /*
    在形参位置定义特定类型的参数,可以在方法中直接使用
     */
    @RequestMapping(value = "/some.do")
    public ModelAndView doSome(HttpServletRequest request,
                               HttpServletResponse response,
                               HttpSession session) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("msg", "service处理后的结果" + request.getParameter("name"));
        mv.setViewName("show");

        return mv;
    }
}

2.10.2 逐个接收

@Controller
@RequestMapping("/activity")
public class MyController {
    /*
    逐个接收:
    需要收到的参数有多少个,就定义多少个形参来接收:
        框架自动赋值,但是需要形参名与参数名一致,与位置无关。如果不一样,则需要在形参前加
        注解@RequestParam("请求参数的名字"),否则会报空指针异常。同时,加了注解的形参必
        须传值,否则网页会出现400异常,除非设置该注解的另一个属性required,该属性默认为真,
        设为false,则对是否传值无要求,如
        @RequestParam(value = "rname", required = false)
        在某些情况中,形参定义的类型不允许空值,如int类型,但是却传进来了空值,
        会发生状态码400错误,此时,将int改为Integer包装类,允许空值的类型即可。
     */
    @RequestMapping(value = "/some.do")
//    public ModelAndView doSome(String name, int age) {
    public ModelAndView doSome(String name, Integer age) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("name", name);
        mv.addObject("age", age);
        mv.setViewName("show");

        return mv;
    }

使用post方式传值时,会遇到乱码的问题,这时可以使用框架自带的过滤器CharacterEncodingFilter解决乱码问题,在web.xml中配置即可:

<!--声明框架自带的过滤器,解决乱码问题-->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>

        <!--重置过滤器声明的三个属性,分别为
        encoding : 字符集

	    forceRequestEncoding : 设为true,强制请求对象使用encoding编码的值

	    forceResponseEncoding : 设为true,强制响应对象使用encoding编码的值-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

2.10.3 对象接收

需要额外创建属性与参数相符的对象类,在vo中创建:

package vo;

public class Student {
    //属性名和请求中的参数名需要保持一致
    private String name;
    private String age;

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

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

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

控制器方法:

	@RequestMapping("/object.do")
    public ModelAndView doObject(Student student) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("name", student.getName());
        mv.addObject("age", student.getAge());
        mv.setViewName("show");

        return mv;
    }

2.11 控制器方法的返回值

处理器方法的返回值表示请求的处理结果

  • ModelAndView:包括数据和视图,对视图执行forward,用于传递数据且跳转新页面。
  • String:仅表示视图,可以是单纯的逻辑名称(文件名),也可以是完整的视图路径(没有视图解析器的情况下),对视图执行forward,用于不传递数据,只跳转新页面。
  • Void(不常用):不需要跳转页面时,可使用void,如ajax请求,其使用响应对象传递请求结果,同时,使用ajax就代表只需要数据而不需要视图(新页面)。
  • Object:仅表示数据,直接在页面显示的数据。加入@ResponseBody注解,可以帮助我们进行响应对象返回数据的操作。

2.11.1 返回值String

@Controller
@RequestMapping("/activity")
public class MyController {
    @RequestMapping(value = "/some.do")
    public String doSome(String name, Integer age) {

        return "show";
    }
}

2.11.2 返回值Void

需要使用ajax,就需要jQuery库文件及Jackson依赖。
在这里插入图片描述

    <!--jackson依赖-->
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.0</version>
  </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.0</version>
    </dependency>

在jsp中引入js库文件:

<script type="text/javascript" src="js/jquery-3.5.1.js"></script>

jsp代码:

    <script type="text/javascript">
        $(function () {
            $("#btn").click(function () {
                //alert("success")
                $.ajax({
                    url:"activity/void.do",
                    data:{
                        name : "zhangsan",
                        age : 22
                    },
                    type:"post",
                    dataType:"json",
                    success: function (resp) {
                        alert(resp)
                    }
                })
            });
        });
    </script>

控制器类方法:

@Controller
@RequestMapping("/activity")
public class MyController {
    @RequestMapping(value = "/void.do")
    public void returnVoid(String name, String age, HttpServletResponse response) throws IOException {
        System.out.println(name + age);
        //假设service已返回一个Student
        Student student = new Student();
        student.setName(name);
        student.setAge(age);

        response.setContentType("application/json;charset=utf-8");
        String json;

        ObjectMapper om = new ObjectMapper();
        json = om.writeValueAsString(student);
        System.out.println(json);

        PrintWriter pw = response.getWriter();
        pw.print(json);

        pw.flush();
        pw.close();
    }

2.11.3 返回值Object

主要步骤:

  • 加入处理json的工具库的依赖,springmvc默认使用jackson
  • 在springmvc配置文件中加入<mvc:annotation-driven>注解驱动,帮助我们完成json字符串的赋值
  • 在控制器类方法上加入@ResponseBody注解,帮助我们完成响应对象设置编码方式,传递数据给浏览器的操作

内部实际上是通过HttpMessageConverter接口完成数据转换。

HttpMessageConverter:消息转换器接口,定义了java转json,转xml等的方法:
canwrite(), write()

其有多种实现类:
常用的StringHttpMessageConverter, MappingJackson2HttpMessageConverter

具体实现了java对象转为json、xml等等对象的转换。

依赖:

    <!--jackson依赖-->
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.0</version>
  </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.0</version>
    </dependency>

注解驱动:

    <mvc:annotation-driven/>

控制器类方法:

    /*
    控制器方法返回Student对象,通过框架辅助转为json,响应浏览器的ajax请求
    @ResponseBody:把控制器方法的返回值转为json,再通过response对象输出给浏览器
        位置:在方法上方,与其他注解没有顺序要求
        返回值是String时,因为有该注解的存在,此时的String表示文本数据,而非视图,此时因为
        返回的不是json格式,所以在ajax中dataType类型指定需要去除,并且此时会产生乱码的现象,
        解决方法是在@RequestMapping注解的属性中指定具体编码格式,如
        @RequestMapping(value = "/object.do", produces = "text/plain;charset=utf-8")
        需要注意的是,此问题无法通过过滤器的方式解决,因为通过@ResponseBody,结果不经过过滤器
     */
    @RequestMapping(value = "/object.do")
    @ResponseBody
    public Student returnObject(String name, String age) {
        //假设service已返回一个Student
        Student student = new Student();
        student.setName(name);
        student.setAge(age);

        return student;
    }

框架的处理流程:

  • 将返回的对象,带入注解驱动创建好的ArrayList中的每个实现类的canwrite()方法,找出返回true的
  • 调用返回true的类的write()方法把对象转换为json
  • 调用@ResponseBody的功能把结果输出到浏览器

同样的,返回对象是集合也可以,在jsp遍历显示即可,是一样的原理。

3. SpringMVC集成Spring集成Mybatis整合开发

  • SpringMVC——界面层
  • Spring——业务层
  • MyBatis——持久层

3.1 选择表文件

3.2 创建web项目

3.3 加依赖,加插件

	<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--springmvc依赖,包含了spring依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--spring依赖,有了springmvc依赖就不需要了-->
<!--    <dependency>-->
<!--      <groupId>org.springframework</groupId>-->
<!--      <artifactId>spring-context</artifactId>-->
<!--      <version>5.2.5.RELEASE</version>-->
<!--    </dependency>-->

    <!--spring事务依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--web项目必须的jsp和servlet依赖-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.1.3-b06</version>
    </dependency>

    <!--jackson依赖-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.9.0</version>
    </dependency>

    <!--mybatis依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.1</version>
    </dependency>

    <!--mybatis-spring集成依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <!--数据库驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.21</version>
    </dependency>

    <!--连接池依赖-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.12</version>
    </dependency>
<build>
    <!--捆绑插件-->
    <resources><!--固定写法,粘贴复制即可-->
      <resource>
        <directory>src/main/java</directory><!--需要告诉Maven一同拷贝的文件所在的目录-->
        <includes><!--使目录下的.properties和.xml文件都被扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
</build>

3.4 编写web.xml

更新web.xml版本:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
  <display-name>crm</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

  <servlet>
    <servlet-name>xxxController</servlet-name>
    <servlet-class>xxx.xxxController</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>xxxController</servlet-name>
    <url-pattern>/xxx/xxx.do</url-pattern>
  </servlet-mapping>
</web-app>

3.4.1 注册SpringMVC的中央调度器DispatcherServlet

记得声明配置文件的位置,及服务器启动时创建的优先级

  <!--注册中央调度器-->
  <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:conf/dispatcherServlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

3.4.2 注册Spring的监听器ContextLoaderListener

记得声明配置文件的位置

  <!--
  注册监听器,用于在服务器启动时创建spring的bean对象,记得使用<context-param>标签
  声明spring配置文件的位置
  -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:conf/applicationContext.xml</param-value>
  </context-param>

3.4.3 注册字符集过滤器

  <!--注册过滤器,重置其属性将字符集改为utf-8-->
  <filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>utf-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceRequestEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
    <init-param>
      <param-name>forceResponseEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

3.5 创建各级目录包

3.6 编写各容器配置文件

3.6.1 SpringMVC配置文件

使用注解开发就需要声明组件扫描器及注解驱动

    <!--声明spring的组件扫描器,用于注解开发-->
    <context:component-scan base-package="controller"/>

    <!--声明mvc的注解驱动-->
    <mvc:annotation-driven/>

    <!--声明视图解析器对象,简化资源文件名,无需id-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

3.6.2 数据库属性的配置文件

jdbc.url=jdbc:mysql://localhost:3306:springmysql/serverTimezone=GMT
jdbc.username=root
jdbc.password=root

3.6.3 Spring配置文件

使用注解开发就需要声明组件扫描器

    <!--
    将数据库的配置信息写在一个独立的配置文件中,便于修改
    通过property-placeholder让spring知道配置文件的位置
    -->
    <context:property-placeholder location="classpath:conf/jdbc.properties"/>

    <!--声明数据源DataSource,连接数据库-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--
    声明mybatis提供的mybatis-spring集成中的SqlSessionFactoryBean类,通过之前声明的数据源,
    结合mybatis的主配置文件,这个类可以在内部创建SqlSessionFactory
    -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:conf/mybatis-config.xml"/>
    </bean>

    <!--
    声明Dao对象:MapperScannerConfigurer:
    通过之前声明的SqlSessionFactory对象可以自动在内部调用getMapper()
    对属性中指定的dao包中的每个dao接口生成代理对象,无需声明id
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <property name="basePackage" value="dao"/>
    </bean>

    <!--声明service-->
    <!--声明组件扫描器,用于注解开发-->
    <context:component-scan base-package="service"/>

3.6.4 mybatis主配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--控制mybatis全局行为-->
    <settings>
        <!--设置mybatis输出日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!--设置别名,这样在dao映射配置文件中的返回类型中,就可以用别名代替全限定名称-->
    <typeAliases>
        <!--name:实体类所在的包名,这样该包下所有的类的类名就是别名-->
        <package name="domain"/>
    </typeAliases>

    <!--因为要使用新的连接池,所以以下这一块就不再需要了-->
    <!--    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mydata"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>-->

    <!--配置dao映射配置文件的位置-->
    <mappers>
        <!--使用mapper就需要每个配置文件都写一次-->
        <!--        <mapper resource="mapper/StudentMapper.xml"/>-->
        <!--使用package则只需要写包名的全限定名称即可,这样,这个包中所有的映射文件一次性全加载-->
        <package name="dao"/>
    </mappers>
</configuration>

3.7 填充各级目录

3.7.1 编写实体类

package domain;

public class Student {
    private Integer id;
    private String name;
    private Integer 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 Integer getAge() {
        return age;
    }

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

3.7.2 编写Dao接口和mapper文件

package dao;

import domain.Student;

import java.util.List;

public interface StudentDao {
    int addStudent(Student student);

    List<Student> queryStudent();
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.StudentDao">
    <insert id="addStudent">
        insert into student(
        name,
        age
        ) values (
        #{name},
        #{age}
        )
    </insert>

    <select id="queryStudent" resultType="Student">
        select id, name, age from student
    </select>
</mapper>

3.7.3 编写service接口及其实现类

package service;

import domain.Student;

import java.util.List;

public interface StudentService {
    int addStudent(Student student);

    List<Student> queryStudent();
}

package service.impl;

import dao.StudentDao;
import domain.Student;
import org.springframework.stereotype.Service;
import service.StudentService;

import javax.annotation.Resource;
import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentDao studentDao;

    @Override
    public int addStudent(Student student) {

        int count = studentDao.addStudent(student);

        return count;
    }

    @Override
    public List<Student> queryStudent() {
        List<Student> sList = studentDao.queryStudent();

        return sList;
    }
}

3.7.4 编写控制器类

package controller;

import domain.Student;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import service.StudentService;

import javax.annotation.Resource;
import java.util.List;

@Controller
public class StudentController {

    @Resource
    private StudentService studentService;

    /*
    在设置了视图解析器的前提下,服务器内部的访问均会受到解析器的影响,
    能访问的资源文件均在解析器的限定之下,那么当我们需要访问非解析器指定的目录中的文件时,
    在视图字符串前加入关键字forward或redirect,通过该关键字,可以令该行代码对资源文件的
    访问不受解析器的影响,如
        mv.setViewName("forward:queryStudent.jsp");
        mv.setViewName("redirect:queryStudent.jsp");
    这时,资源文件的名称就要求是有前缀路径和后缀扩展名的格式
     */
    @RequestMapping("/addStudent.do")
    public ModelAndView addStudent(Student student) {
        ModelAndView mv = new ModelAndView();
        String tips = "注册失败";

        int count = studentService.addStudent(student);
        if (count > 0) {
            tips = "学生" + student.getName() + "注册成功";
        }
        mv.addObject("msg", tips);
        mv.setViewName("result");

        return mv;
    }

    @RequestMapping("/queryStudent.do")
    @ResponseBody
    public List<Student> queryStudent() {

        List<Student> sList = studentService.queryStudent();

        return sList;
    }
}

在关键字重定向中,框架会把Model中的简单类型的数据,转为string,作为目标jsp页面的请求参数使用,这样就可以在重定向的转发类型之下完成数据的传递。

在目标jsp页面中可以使用EL表达式${param.key}的方式获得数据值。另外,重定向方式不能访问WEB-INF/下的资源

3.8 编写jsp页面

请求页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
    <base href="<%=basePath%>">
    <title>学生管理</title>
</head>
<body>
<table>
    <tr>
        <td><a href="addStudent.jsp">注册</a></td>
    </tr>
    <tr>
        <td><a href="queryStudent.jsp"> 查询</a></td>
    </tr>
</table>
</body>
</html>

跳转页面addStudent.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
    <base href="<%=basePath%>">
    <title>Title</title>
</head>
<body>
<form action="addStudent.do" method="post">
    <table>
        <tr>
            <td><input type="text" name="name" placeholder="姓名"/> </td>
        </tr>
        <tr>
            <td><input type="text" name="age" placeholder="年龄"/> </td>
        </tr>
        <tr>
            <td><input type="submit" value="提交"/> </td>
        </tr>
    </table>
</form>
</body>
</html>

跳转页面queryStudent.jsp:
需要用到ajax的情况下,需要先引入js库,然后确保jackson依赖已添加,再加入引用代码才能使用。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
    <base href="<%=basePath%>">
    <title>Title</title>
    <script type="text/javascript" src="js/jquery-3.5.1.js"></script>
    <script>
        $(function () {
            $("#btn").click(function () {
                queryStudent();
            });

            queryStudent();
        });

        function queryStudent() {
            $.ajax({
                url:"queryStudent.do",
                type:"post",
                dataType:"json",
                success: function (resp) {
                    $("#info").html("");
                    $.each(resp, function (index, element) {
                        $("#info").append("<tr>")
                            .append("<td>" + element.id + "</td>")
                            .append("<td>" + element.name + "</td>")
                            .append("<td>" + element.age + "</td>")
                            .append("</tr>")
                    });
                }
            })
        }
    </script>
</head>
<body>
<input type="button" id="btn" value="查询所有学生信息">
<div id="div">
    <table id="table">
        <thead>
        <tr>
            <td>编号</td>
            <td>姓名</td>
            <td>年龄</td>
        </tr>
        </thead>
        <tbody id="info"></tbody>
    </table>
</div>
</body>
</html>

4. SpringMVC核心技术

4.1关键字:重定向和转发

在设置了视图解析器的前提下,服务器内部的访问均会受到解析器的影响,能访问的资源文件均在解析器的限定之下,那么当我们需要访问非解析器指定的目录中的文件时,在视图字符串前加入关键字forward或redirect,通过该关键字,可以令该行代码对资源文件的访问不受解析器的影响,如
mv.setViewName(“forward:queryStudent.jsp”);
mv.setViewName(“redirect:queryStudent.jsp”);
这时,资源文件的名称就要求是有前缀路径和后缀扩展名的格式。

4.2 异常处理

自定义异常类,在类名上加入注解@ControllerAdvice,在方法名上加入注解@ExceptionHandler(value=“异常类.class”),可创建一个处理所有异常的异常处理类,在方法上的注解中注明处理的异常类型。这样做的好处是可以将异常处理代码与源代码分离,不用在源代码中写try/catch,体现了aop,解耦合的思想。同样的,这样的对象的创建也在mvc配置文件中完成。

自定义异常类:

package exception;

public class UserException extends Exception {
    public UserException() {
        super();
    }

    public UserException(String message) {
        super(message);
    }
}

package exception;

public class NameException extends UserException{
    public NameException() {
        super();
    }

    public NameException(String message) {
        super(message);
    }
}

package exception;

public class AgeException extends UserException{
    public AgeException() {
        super();
    }

    public AgeException(String message) {
        super(message);
    }
}

定义全局异常处理类:

package handler;

import exception.AgeException;
import exception.NameException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
public class GlobalExceptionHandler {

    /*
    该方法与处理器方法相同,返回值有多种。
    形参exception,必填,表示Controller抛出的异常对象。
    异常处理逻辑:
    1.在日志中记录异常,发生时间,发生在哪里,异常错误内容
    2.发送通知,邮件/短信/微信通知相关人员
    3.给用户提示
     */
    @ExceptionHandler(NameException.class)
    public ModelAndView doNameException(Exception exception) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("nameException", exception);
        mv.setViewName("NameError");

        return mv;
    }

    @ExceptionHandler(AgeException.class)
    public ModelAndView doAgeException(Exception exception) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("ageException", exception);
        mv.setViewName("AgeError");

        return mv;
    }

    /*
    配备属性为空的注解的方法作为所有其他异常的处理方法
     */
    @ExceptionHandler()
    public ModelAndView doOther(Exception exception) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("exception", exception);
        mv.setViewName("DefaultError");

        return mv;
    }
}

控制器方法:

package controller;

import exception.AgeException;
import exception.NameException;
import exception.UserException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class StudentController {

    @RequestMapping("/exception.do")
    public ModelAndView doException(String name, Integer age) throws UserException {
        if (!"zs".equals(name)) {
            throw new NameException("只有zs可以登录");
        }
        if (age > 80) {
            throw new AgeException("年龄不能大于80");
        }

        ModelAndView mv = new ModelAndView();
        mv.addObject("name", name);
        mv.addObject("age", age);
        mv.setViewName("result");

        return mv;
    }
}

mvc配置文件声明组件扫描器:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--声明spring的组件扫描器,用于注解开发-->
    <context:component-scan base-package="controller"/>
    <context:component-scan base-package="handler"/>

    <!--声明mvc的注解驱动-->
    <mvc:annotation-driven/>

    <!--声明视图解析器对象,简化资源文件名,无需id-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

4.3 拦截器

自定义拦截器类实现框架中的HandlerInterceptor接口,在mvc配置文件中声明,指定需要拦截的uri地址。

多用在用户登录处理,权限检查,记录日志等模块。

其实是多个Controller中的功能集中到一处,同样体现了aop的思想

拦截器和过滤器的区别:

  • 过滤器是servlet中的对象,拦截器是框架中的对象
  • 过滤器实现Filter接口,拦截器实现HandlerInterceptor
  • 过滤器在拦截器之前执行
  • 过滤器是tomcat创建的,拦截器是springmvc创建的
  • 过滤器只有一个执行时间点,拦截器有三个执行时间点
  • 过滤器可以处理多个文件:jsp,js,html等,拦截器只拦截对Controller的访问,如果你的请求无法被中央调度器接收,就也不会执行拦截器

拦截器:

package handler;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class Interceptor implements HandlerInterceptor {

    /*
    Object handler:被拦截的控制器对象
    该方法在控制器执行前执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object object = request.getSession().getAttribute("name");
        String user = "";
        if (object != null) {
            user = (String) object;
        }

        if (!"zs".equals(user)) {
            request.getRequestDispatcher("/tips.jsp").forward(request, response);
            return false;
        }

        return true;
    }

    /*
    ModelAndView modelAndView:处理器方法的返回值
    可对控制器方法产生的结果进行修改,影响最后的执行结果。
    主要作用是对结果进行二次修正
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /*
    Exception ex:程序中发生的异常
    一般用于资源回收
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

模拟用户登录:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
    <base href="<%=basePath%>">
    <title>账户登录</title>
</head>
<body>
<%
    request.getSession().setAttribute("name", "zs");
%>
</body>
</html>

模拟用户注销:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
    <base href="<%=basePath%>">
    <title>账户注销</title>
</head>
<body>
<%
    request.getSession().removeAttribute("name");
%>
</body>
</html>

配置拦截器:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--声明spring的组件扫描器,用于注解开发-->
    <context:component-scan base-package="controller"/>
    <context:component-scan base-package="handler"/>

    <!--声明mvc的注解驱动-->
    <mvc:annotation-driven/>

    <!--声明视图解析器对象,简化资源文件名,无需id-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--
    声明拦截器
    多个拦截器在框架内以List集合的形式保存,先声明的先保存,先拦截
    -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--指定拦截的uri地址,可使用通配符-->
            <mvc:mapping path="/**"/>
            <!--声明拦截器对象-->
            <bean class="handler.Interceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值