黑马程序员--SpringMVC详细教程

  • 一、SpringMVC概述

        SpringMVC 是一种基于 Java 的实现 MVC 设计模型的请求驱动类型的轻量级 Web 框架,属于 SpringFrameWork 的后续产品,已经融合在 Spring Web Flow 中。

        SpringMVC 已经成为目前最主流的MVC框架之一,并且随着Spring3.0 的发布,全面超越 Struts2,成为最优 秀的 MVC 框架。它通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口。同时 它还支持 RESTful 编程风格的请求。

二、SpringMVC快速入门你

        1)SpringMVC流程图示

        2)SpringMVC的开发步骤

                        ① 导入SpringMVC相关坐标

                        ② 配置SpringMVC核心控制器DispathcerServlet

                        ③ 创建Controller类和视图页面

                        ④ 使用注解配置Controller类中业务方法的映射地址

                        ⑤ 配置SpringMVC核心文件 spring-mvc.xml

                        ⑥ 客户端发起请求测试 

         ① 导入SpringMVC相关坐标

         ① 导入Servlet和Jsp的坐标

<!--Spring坐标-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>

<!--SpringMVC坐标-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>

<!--Servlet坐标-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>

<!--Jsp坐标-->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
</dependency>

        ② 在web.xml配置SpringMVC的核心控制器DispathcerServlet

<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:spring-mvc.xml</param-value>
    </init-param>
 <!--
        <load-on-startup>1</load-on-startup>的作用
        1)load-on-startup元素标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。
        2)它的值必须是一个整数,表示servlet应该被载入的顺序
        3)当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet;
        4)当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载。
        5)正数的值越小,该servlet的优先级越高,应用启动时就越先加载。
        6)当值相同时,容器就会自己选择顺序来加载。
        -->
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

         ③ 创建Controller类和视图页面

public class QuickController {
public String quickMethod(){
    System.out.println("quickMethod running.....");
    return "index.jsp";
}
}

        ④ 使用注解配置Controller类中业务方法的映射地址

@Controller
public class QuickController {
    @RequestMapping("/quick")
    public String quickMethod(){
        System.out.println("quickMethod running.....");
        return "index.jsp";
    }
}

         ⑤ 配置SpringMVC核心文件 spring-mvc.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置注解扫描-->
    <context:component-scan base-package="com.itheima"/>
</beans>

三、SpringMVC组件解析

1)SpringMVC的执行流程

① 用户发送请求至前端控制器DispatcherServlet。

② DispatcherServlet收到请求调用HandlerMapping处理器映射器。

③ 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果 有则生成)一并返回给DispatcherServlet。

④ DispatcherServlet调用HandlerAdapter处理器适配器。

⑤ HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

⑥ Controller执行完成返回ModelAndView。

⑦ HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

⑧ DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

⑨ ViewReslover解析后返回具体View。

⑩ DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。DispatcherServlet响应用户。

        2)SpringMVC组件解析

        1. 前端控制器:DispatcherServlet

        用户请求到达前端控制器,它就相当于 MVC 模式中的 C,DispatcherServlet 是整个流程控制的中心,由 它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。

        2. 处理器映射器:HandlerMapping HandlerMapping

        负责根据用户请求找到 Handler 即处理器,SpringMVC 提供了不同的映射器实现不同的 映射方式,例如:配置文件方式,实现接口方式,注解方式等。

        3. 处理器适配器:HandlerAdapter

        通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理 器进行执行。

        4. 处理器:Handler

        它就是我们开发中要编写的具体业务控制器。由 DispatcherServlet 把用户请求转发到 Handler。由 Handler 对具体的用户请求进行处理。

        5. 视图解析器:View Resolver

        View Resolver 负责将处理结果生成 View 视图,View Resolver 首先根据逻辑视图名解析成物理视图名,即 具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。

        6. 视图:View

        SpringMVC 框架提供了很多的 View 视图类型的支持,包括:jstlView、freemarkerView、pdfView等。最 常用的视图就是 jsp。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程 序员根据业务需求开发具体的页面

        3)SpringMVC注解解析

        @RequestMapping

        作用:用于建立请求 URL 和处理请求方法之间的对应关系

        位置:

  • 类上,请求URL 的第一级访问目录。此处不写的话,就相当于应用的根目录
  • 方法上,请求 URL 的第二级访问目录,与类上的使用@ReqquestMapping标注的一级目录一起组成访问虚拟路径

        属性:

  • value:用于指定请求的URL。它和path属性的作用是一样的
  • method:用于指定请求的方式
  • params:用于指定限制请求参数的条件。它支持简单的表达式。要求请求参数的key和value必须和配置的一模一样

        例如

        ①params = {"accountName"},表示请求参数必须有accountName

        ②params = {"moeny!100"},表示请求参数中money不能是100

        4)SpringMVC的XML配置解析

        ①视图解析器

        REDIRECT_URL_PREFIX  = "redirect:"         --重定向前缀        

         FORWARD_URL_PREFIX = "forward:"         --转发前缀(默认值)

        prefix = " ";         --视图名称前缀

        suffix = " ";         --视图名称后缀

我们可以通过属性注入的方式修改视图的的前后缀

<!--配置内部资源视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--prefix = "" --视图名称前缀    suffix = ""--视图名称后缀 -->
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
</bean>

四、SpringMVC的数据响应

        写配置时一定要先加上mvc的注解驱动

        在方法上添加@ResponseBody就可以返回json格式的字符串。

        我们可以使用mvc的注解驱动代替配置,让SpringMVC自动将User转换成json格式的字符串。

 1)SpringMVC的数据响应方式

        1、页面跳转

                ①直接返回字符串

                ② 通过ModelAndView对象返回

        2、回写数据

               ① 直接返回字符串

               ② 返回对象或集合

2)页面跳转

        ①ModelAndView对象

        它可以通过ModelAndView的addObject()方法设置,向request域存储数据。也可以进行页面跳转。

 @RequestMapping(value="/quick3")
 //如果传有ModelAndView作为形参,MVC框架会帮你注入,那么MVC会将自动给你创建一个ModelAndView对象供你使用
    public ModelAndView save3(ModelAndView modelAndView){
        modelAndView.addObject("username","itheima");
        modelAndView.setViewName("success");
        return modelAndView;
    }

        ②返回字符串形式

        直接返回字符串:此种方式会将返回的字符串与视图解析器的前后缀拼接后跳转

 

3)回写数据 

        返回对象或集合,或者字符串

        在方法上添加@ResponseBody就可以返回json格式的字符串。 因此,我们可以使用mvc的注解驱动代替配置,让SpringMVC自动将User转换成json格式的字符串。

<?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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

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

</beans>

        在 SpringMVC 的各个组件中,处理器映射器、处理器适配器、视图解析器称为 SpringMVC 的三大组件。 使用自动加载 RequestMappingHandlerMapping(处理映射器)和 RequestMappingHandlerAdapter( 处 理 适 配 器 ),可用在Spring-xml.xml配置文件中使用 替代注解处理器和适配器的配置。

        同时使用默认底层就会集成jackson进行对象或集合的json格式字符串的转换。


@RequestMapping("/quick7")
@ResponseBody    //告知SpringMVC框架 不进行视图跳转 直接进行数据响应
public String quickMethod7() throws IOException {
    User user = new User();
    user.setUsername("zhangsan");
    user.setAge(18);
  
    return user;
}

五、SpringMVC获得请求数据

        MVC实现数据请求方式

  • 基本类型参数
  • POJO类型参数
  • 数组类型参数
  •  集合类型参数

        MVC获取数据细节

  • 中文乱码问题
  • @RequestParam 和 @PathVariable
  • 自定义类型转换器
  • 获得Servlet相关API
  • @RequestHeader 和 @CookieValue
  • 文件上传

1)基本类型参数,POJO类型参数和数组类型参数的获取

Controller中的业务方法数组名称与请求参数的name一致,参数值会自动映射匹配。

//数组数据类型的获取

@RequestMapping("/quick11")
@ResponseBody
public void quickMethod11(String[] strs) throws IOException {
    System.out.println(Arrays.asList(strs));
}

//POJO数据类型的获取

public class User {
private String username;
private int age;
getter/setter…
}
@RequestMapping("/quick10")
@ResponseBody
public void quickMethod10(User user) throws IOException {
    System.out.println(user);
}

//基本数据类型的获取

@RequestMapping("/quick9")
@ResponseBody
public void quickMethod9(String username,int age) throws IOException {
    System.out.println(username);
    System.out.println(age);
}

2)获得集合类型参数

        一般情况:获得集合参数时,要将集合参数包装到一个POJO中才可以。

//用户类
public class User {

    private String username;
    private int age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}
//用于封装的类
public class VO {

    private List<User> userList;

    public List<User> getUserList() {
        return userList;
    }

    public void setUserList(List<User> userList) {
        this.userList = userList;
    }

    @Override
    public String toString() {
        return "VO{" +
                "userList=" + userList +
                '}';
    }
}
<!--前端表单,userList[0].username-----userList要与封装类中的属性名对应,[0]表示第几个对象,username表示对象中的属性。-->
<form action="${pageContext.request.contextPath}/quick12" method="post">
    <input type="text" name="userList[0].username"><br>
    <input type="text" name="userList[0].age"><br>
    <input type="text" name="userList[1].username"><br>
    <input type="text" name="userList[1].age"><br>
    <input type="submit" value="提交"><br>
</form>
//测试程序
@RequestMapping("/quick12")
@ResponseBody
public void quickMethod12(Vo vo) throws IOException {
    System.out.println(vo.getUserList());
}

特殊情况:

        使用ajax提交时,可以指定contentType为json形式,那么在方法参数位置使用@RequestBody可以 直接接收集合数据而无需使用POJO进行包装。

<script>
    //模拟数据
    var userList = new Array();
    userList.push({username: "zhangsan",age: "20"});
    userList.push({username: "lisi",age: "20"});
    $.ajax({
        type: "POST",
        url: "/itheima_springmvc1/quick13",
        data: JSON.stringify(userList),
        contentType : 'application/json;charset=utf-8'
    });
</script>
@RequestMapping("/quick13")
@ResponseBody
public void quickMethod13(@RequestBody List<User> userList) throws 
IOException {
    System.out.println(userList);
}

3)设置开放静态资源

        通过谷歌开发者工具抓包发现,没有加载到jquery文件,原因是SpringMVC的前端控制器 DispatcherServlet的url-pattern配置的是/,代表对所有的资源都进行过滤操作,

我们可以通过以下两种方式指定放行静态资源:

• 在spring-mvc.xml配置文件中指定放行的资源

<mvc:resources mapping="/js/**" location="/js/"/> 

• 使用标签

<mvc:default-servlet-handler/>

4)请求数据乱码问题

        当post请求时,数据会出现乱码,我们可以设置一个过滤器来进行编码的过滤。

         

<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>
    </filter>

<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

5)参数绑定注解@requestParam

        注解@RequestParam还有如下参数可以使用:

  • value:与请求参数名称
  • required:此在指定的请求参数是否必须包括,默认是true,提交时如果没有此参数则报错
  • defaultValue:当没有指定请求参数时,则使用指定的默认值赋值

        其实就是设置个别名,来获取表单中的数据 

<!--前端表单-->
<form action="${pageContext.request.contextPath}/quick14" method="post">
    <input type="text" name="name"><br>
    <input type="submit" value="提交"><br>
</form>
@RequestMapping("/quick14")
@ResponseBody
public void quickMethod14(@RequestParam("name") String username) throws 
IOException {
    System.out.println(username);
}

 6)获得Restful风格的参数

        Restful是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。主要用于客户端和服务 器交互类的软件,基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存机制等。

         Restful风格的请求是使用“url+请求方式”表示一次请求目的的,HTTP 协议里面四个表示操作方式的动词如下:

  • GET:用于获取资源 
  • POST:用于新建资源 
  • PUT:用于更新资源 
  • DELETE:用于删除资源

例如:

  • /user/1 GET : 得到 id = 1 的 user
  • /user/1 DELETE: 删除 id = 1 的 user
  •  /user/1 PUT: 更新 id = 1 的 user
  • /user POST: 新增 user

        上述url地址/user/1中的1就是要获得的请求参数,在SpringMVC中可以使用占位符进行参数绑定。

        地址/user/1可以写成 /user/{id},占位符{id}对应的就是1的值。在业务方法中我们可以使用@PathVariable注解进行占位符的匹配获取工作。

7)自定义类型转换器

  • SpringMVC 默认已经提供了一些常用的类型转换器,例如客户端提交的字符串转换成int型进行参数设置。
  • 但是不是所有的数据类型都提供了转换器,没有提供的就需要自定义转换器,例如:日期类型的数据就需要自 定义转换器。

        自定义类型转换器的开发步骤:

                ① 定义转换器类实现Converter接口

                ② 在配置文件中声明转换器

                ③ 在中引用转换器

         ① 定义转换器类实现Converter接口

//第一个参数是接受的参数类型,第二个参数是要转换成的类型
public class DateConverter implements Converter<String, Date> {

    public Date convert(String dateStr) {
        //将日期字符串转换成日期对象 返回
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        Date date = null;
        try {
            date = format.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

② 在配置文件中声明转换器

<bean id="converterService" 
    class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
        <bean class="com.itheima.converter.DateConverter"/>
        </list>
    </property>
</bean>

③ 在<annotation-driven>中引用转换器

<mvc:annotation-driven conversion-service="converterService"/>

8)获得Servlet相关API

SpringMVC支持使用原始ServletAPI对象作为控制器方法的参数进行注入,常用的对象如下:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
@RequestMapping("/quick16")
@ResponseBody
public void quickMethod16(HttpServletRequest request,HttpServletResponse 
response,
HttpSession session){
    System.out.println(request);
    System.out.println(response);
    System.out.println(session);
}

9)获得请求头

        ① @RequestHeader

        使用@RequestHeader可以获得请求头信息,相当于web阶段学习的request.getHeader(name) @RequestHeader注解的属性如下:

  • value:请求头的名称
  • required:是否必须携带此请求头
@RequestMapping("/quick17")
@ResponseBody
public void quickMethod17(@RequestHeader(value = "User-Agent",required = false) String headerValue){
    System.out.println(headerValue);
}

② @CookieValue

        使用@CookieValue可以获得指定Cookie的值

        @CookieValue注解的属性如下:

  • value:指定cookie的名称
  • required:是否必须携带此cookie
@RequestMapping("/quick18")
@ResponseBody
public void quickMethod18(@CookieValue(value = "JSESSIONID",required = false) String jsessionid){
    System.out.println(jsessionid);
}

10)文件上传

        ①文件上传客户端三要素

  • 表单项type=“file”
  • 表单的提交方式是post
  • 表单的enctype属性是多部分表单形式,及enctype=“multipart/form-data”
<form action="${pageContext.request.contextPath}/quick20" method="post" 
enctype="multipart/form-data">
    名称:<input type="text" name="name"><br>
    文件:<input type="file" name="file"><br>
    <input type="submit" value="提交"><br>
</form>

        ② 文件上传原理

  • 当form表单修改为多部分表单时,request.getParameter()将失效。
  • enctype=“application/x-www-form-urlencoded”时,form表单的正文内容格式是: key=value&key=value&key=value
  • 当form表单的enctype取值为Mutilpart/form-data时,请求正文内容就变成多部分形式:

        ③单文件上传步骤

                ① 导入fileupload和io坐标

                ② 配置文件上传解析器

                ③ 编写文件上传代码

① 导入fileupload和io坐标

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.2.2</version>
</dependency>

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

② 配置文件上传解析器

<bean id="multipartResolver" 
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--上传文件总大小-->
    <property name="maxUploadSize" value="5242800"/>
    <!--上传单个文件的大小-->
    <property name="maxUploadSizePerFile" value="5242800"/>
    <!--上传文件的编码类型-->
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

③ 编写文件上传代码

@RequestMapping("/quick20")
@ResponseBody
public void quickMethod20(String name,MultipartFile uploadFile) throws 
IOException {
    //获得文件名称
    String originalFilename = uploadFile.getOriginalFilename();
    //保存文件
    uploadFile.transferTo(new File("C:\\upload\\"+originalFilename));
}

        ④多文件上传实现

        多文件上传,只需要将页面修改为多个文件上传项,将方法参数MultipartFile类型修改为MultipartFile[]即可,然后用for循环对每个文件文件进行获取和保存。

<h1>多文件上传测试</h1>
<form action="${pageContext.request.contextPath}/quick21" method="post" 
enctype="multipart/form-data">
    名称:<input type="text" name="name"><br>
    文件1:<input type="file" name="uploadFiles"><br>
    文件2:<input type="file" name="uploadFiles"><br>
    文件3:<input type="file" name="uploadFiles"><br>
    <input type="submit" value="提交"><br>
</form>
@RequestMapping("/quick21")
@ResponseBody
public void quickMethod21(String name,MultipartFile[] uploadFiles) throws 
IOException {
    for (MultipartFile uploadFile : uploadFiles){
        String originalFilename = uploadFile.getOriginalFilename();
        uploadFile.transferTo(new File("C:\\upload\\"+originalFilename));
    }
}

六、Spring JdbcTemplate基本使用

1)概述

        它是spring框架中提供的一个对象,是对原始繁琐的Jdbc API对象的简单封装。spring框架为我们提供了很多的操作 模板类。例如:操作关系型数据的JdbcTemplate和HibernateTemplate,操作nosql数据库的RedisTemplate,操 作消息队列的JmsTemplate等等。

 2)JdbcTemplate开发步骤

                ① 导入spring-jdbc和spring-tx坐标

                ② 创建数据库表和实体

                ③ 创建JdbcTemplate对象

                ④ 执行数据库操作

         ① 导入spring-jdbc和spring-tx坐标

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.0.5.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.0.5.RELEASE</version>
</dependency>

        ②创建数据库表和实体

public class user {
    private String user;
    private String password;
    private int balance;
//省略get和set等方法
}

        ③ 创建JdbcTemplate对象

        我们可以将JdbcTemplate的创建权交给Spring,将数据源DataSource的创建权也交给Spring,在Spring容器内部将 数据源DataSource注入到JdbcTemplate模版对象中,配置如下:

 <!--加载外部的properties文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--产生数据源对象-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!--        name="填的是set后面这一些,把大写改成小写"-->
        <property name="driverClassName" value="${jdbc.driverClassName}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!--jdbc模板对象-->
    <!--JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

  ④ 执行数据库操作

        @ContextConfiguration这个注解通常与@RunWith(SpringJUnit4ClassRunner.class)联合使用用来测试当一个类添加了注解@Component,那么他就自动变成了一个bean,就不需要再Spring配置文件中显示的配置了。把这些    bean收集起来通常有两种方式,Java的方式和XML的方式。当这些bean收集起来之后,当我们想要在某个测试类使用@Autowired注解来引入这些收集起来的bean时,只需要给这个测试类添加@ContextConfiguration注解来标注我们想要导入这个测试类的某些bean。

        常用操作:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class JdbcTemplateCRUDTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    
    //查询一条记录
@Test
    public void testQueryOne() {

    Long count = jdbcTemplate.queryForObject()
    }

    //查询所有记录
    @Test
    public void testQueryAll() {
        List<user> userList = jdbcTemplate.query("select * from user_table",
                new BeanPropertyRowMapper<user>(user.class));
        System.out.println(userList);
    }


    //更新数据操作
    @Test
    public void testUpdate() {
        jdbcTemplate.update("update user_table set password=? where  user=?", "qq1111111", "DD");
    }

    
    //删除操作
    @Test
    public void testDelete() {
        jdbcTemplate.update("delete from user_table where user=?", "qq");
    }


}
@Test
public void test2(){
        ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
       JdbcTemplate jdbcTemplate=app.getBean(JdbcTemplate.class);
        int row = jdbcTemplate.update("insert into user_table value (?,?,?)","qq","123456",1252);
        System.out.println(row);
}

七、SpringMVC拦截器(interceptor)

1)拦截器(interceptor)的作用

        Spring MVC 的拦截器类似于 Servlet 开发中的过滤器 Filter,用于对处理器进行预处理和后处理。

        将拦截器按一定的顺序联结成一条链,这条链称为拦截器链(Interceptor Chain)。在访问被拦截的方 法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用。拦截器也是AOP思想的具体实现。

2)拦截器方法说明

 

3)自定义拦截器的开发步骤

                ① 创建拦截器类实现HandlerInterceptor接口

                ② 配置拦截器

                ③ 测试拦截器的拦截效果

 ① 创建拦截器类实现HandlerInterceptor接口



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

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyInterceptor1 implements HandlerInterceptor {
    //在目标方法执行之前 执行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
      //设置需要的程序
    }

    //在目标方法执行之后 视图对象返回之前执行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
         //设置需要的程序
    }

    //在流程都执行完毕后 执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
         //设置需要的程序
    }
}

 ② 配置拦截器

   <!--配置拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--对哪些资源执行拦截操作-->
            <mvc:mapping path="/**"/>
            <bean class="com.itheima.interceptor.MyInterceptor2"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <!--对哪些资源执行拦截操作-->
            <mvc:mapping path="/**"/>
            <bean class="com.itheima.interceptor.MyInterceptor1"/>
        </mvc:interceptor>
    </mvc:interceptors>

 

八、SpringMVC异常处理

1)异常处理的思路

        系统中异常包括两类:预期异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后 者主要通过规范代码开发、测试等手段减少运行时异常的发生。

        系统的Dao、Service、Controller出现都通过throws Exception向上抛出,最后由SpringMVC前端控制器交 由异常处理器进行异常处理,如下图:

 

2)异常处理两种方式

  • 使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver
  • 实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器

3)简单异常处理器SimpleMappingExceptionResolver

        SpringMVC已经定义好了该类型转换器,在使用时可以根据项目情况进行相应异常与视图的映射配置

 

4)自定义异常处理步骤

                ① 创建异常处理器类实现HandlerExceptionResolver

                ② 配置异常处理器

                ③ 编写异常页面

                ④ 测试异常跳转

        ① 创建异常处理器类实现HandlerExceptionResolver

public class MyExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, 
HttpServletResponse response, Object handler, Exception ex) {
//处理异常的代码实现
//创建ModelAndView对象
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("exceptionPage");
return modelAndView;
}
}

② 配置异常处理器

<bean id="exceptionResolver" 
class="com.itheima.exception.MyExceptionResolver"/>

③ 编写异常页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
这是一个最终异常的显示页面
</body>
</html>

④ 测试异常跳转

@RequestMapping("/quick22")
@ResponseBody
public void quickMethod22() throws IOException, ParseException {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    simpleDateFormat.parse("abcde");
}

九、Spring 的 AOP

1)什么是 AOP及其作用和优势

        AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理 实现程序功能的统一维护的一种技术。

         AOP 是 OOP(面向对象) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍 生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序 的可重用性,同时提高了开发的效率。

        作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强

        优势:减少重复代码,提高开发效率,并且便于维护

 

2)AOP 的底层实现

        实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态 的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强

3) AOP 的动态代理技术

        常用的动态代理技术

  • JDK 代理 : 基于接口的动态代理技术
  • cglib 代理:基于父类的动态代理技术

 

 4)AOP 相关概念

Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编 写,并通过配置的方式完成指定目标的方法增强。

在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:

  • Target(目标对象):代理的目标对象(被增强的对象)
  • Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点(可以被增强的方法)
  • Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义(真正配置去增强的方法,范围比连接点小)
  • Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知(封装增强业务逻辑的方法)
  • Aspect(切面):是切入点和通知(引介)的结合(切点+增强(通知))
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而 AspectJ采用编译期织入和类装载期织入(增强和切点结合的过程叫做织入,通过配置实现)

5)AOP 开发明确的事项 

        1. 需要编写的内容

  • 编写核心业务代码(目标类的目标方法)
  • 编写切面类,切面类中有通知(增强功能方法)
  • 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合         

        2. AOP 技术实现的内容

        Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的 代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

        3. AOP 底层使用哪种代理方式

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

6) 基于 XML 的 AOP 开发快速入门

        1.开发步骤

                ① 导入 AOP 相关坐标

                ② 创建目标接口和目标类(内部有切点)

                ③ 创建切面类(内部有增强方法)

                ④ 将目标类和切面类的对象创建权交给 spring

                ⑤ 在 applicationContext.xml 中配置织入关系

                ⑥ 测试代码

① 导入 AOP 相关坐标

<!--导入spring的context坐标,context依赖aop-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>

  

② 创建目标接口和目标类(内部有切点)

//接口类
public interface TargetInterface {

    public void save();

}
//实现类(目标类)
public class Target implements TargetInterface {
    public void save() {
        System.out.println("save running.....");
   
    }
}

③ 创建切面类(内部有增强方法)

public class MyAspect {
//前置增强方法
public void before(){
    System.out.println("前置代码增强.....");
    }
}

④ 将目标类和切面类的对象创建权交给 spring

<!--配置目标类-->
<bean id="target" class="com.itheima.aop.Target"></bean>
<!--配置切面类-->
<bean id="myAspect" class="com.itheima.aop.MyAspect"></bean>

⑤ 在 applicationContext.xml 中配置织入关系

        1.导入aop命名空间

        2. 配置切点表达式和前置增强的织入关系

 

<!--配置织入:告诉spring框架 哪些方法(切点)需要进行哪些增强(前置、后置...)-->
    <aop:config>
     <!--引用myAspect的Bean为切面对象-->
    <aop:aspect ref="myAspect">
<!--配置Target的method方法执行时要进行myAspect的before方法前置增强-->
    <aop:before method="before" pointcut="execution(public void com.itheima.aop.Target.method())"></aop:before>
    </aop:aspect>
</aop:config>

7)XML 配置 AOP 详解

① 切点表达式的写法

 

        表达式语法:

        execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  • 访问修饰符可以省略
  • 返回值类型、包名、类名、方法名可以使用星号* 代表任意
  • 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
  • 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表

例如:

execution(public void com.itheima.aop.Target.method())   com包下的itheima包下的aop包下的Target类下的method方法(无参)并且无返回值

execution(void com.itheima.aop.Target.*(..))   com包下的itheima包下的aop包下的Target类下的任意方法并且是任意参数(有参无参都可)并且无返回值

execution(* com.itheima.aop.*.*(..))   com包下的itheima包下的aop包下的任意类的任意方法并且是任意参数(有参无参都可)并且是什么返回值都可以

execution(* com.itheima.aop..*.*(..))  com包下的itheima包下的aop包及其子包下的任意类的任意方法并且是任意参数(有参无参都可)并且是什么返回值都可以

execution(* *..*.*(..)) 任意包的任意类的任意方法

        ②通知的类型

                通知的配置语法:

<aop:通知类型 method=“切面类中方法名” pointcut=“切点表达式"></aop:通知类型>

 

        ③切点表达式的抽取

        当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽 取后的切点表达式。

 

 <!--配置织入:告诉spring框架 哪些方法(切点)需要进行哪些增强(前置、后置...)-->
    <aop:config>
        <!--声明切面-->
        <aop:aspect ref="myAspect">
            <!--抽取切点表达式-->
            <aop:pointcut id="myPointcut" expression="execution(* com.itheima.aop.*.*(..))"></aop:pointcut><aop:around method="around" pointcut-ref="myPointcut"/>

            <aop:after method="after" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>

8)基于注解的 AOP 开发

        1.开发步骤

                ① 创建目标接口和目标类(内部有切点)

                ② 创建切面类(内部有增强方法)

                ③ 将目标类和切面类的对象创建权交给 spring(使用@Aspect标注切面类

                ④ 在切面类中使用注解配置织入关系(使用@通知注解标注通知方法

                ⑤ 在配置文件中开启组件扫描和 AOP 的自动代理(在配置文件中配置aop自动代理

                ⑥ 测试

        ① 创建目标接口和目标类(内部有切点)

//接口类
public interface TargetInterface {

    public void save();

}
//实现类(切点)
@Component("target")
public class Target implements TargetInterface {
    public void save() {
        System.out.println("save running.....");
        //int i = 1/0;
    }
}

② 创建切面类(内部有增强方法)

public class MyAspect {
//前置增强方法
public void before(){
    System.out.println("前置代码增强.....");
    }
}

③ 将目标类和切面类的对象创建权交给 spring(@Component(""))

@Component("target")
public class Target implements TargetInterface {

//目标类
@Override
public void method() {
    System.out.println("Target running....");
}
}

//切面类
@Component("myAspect")
public class MyAspect {
    public void before(){
        System.out.println("前置代码增强.....");
    }
}

④ 在切面类中使用注解配置织入关系(@Aspect)

@Component("myAspect")
@Aspect
public class MyAspect {

@Before("execution(* com.itheima.aop.*.*(..))")
public void before(){
    System.out.println("前置代码增强.....");
    }
}

      

⑤ 在配置文件中开启组件扫描和 AOP 的自动代理

    <!--组件扫描-->
    <context:component-scan base-package="com.itheima.anno"/>

    <!--aop自动代理-->
    <aop:aspectj-autoproxy/>

9)注解配置 AOP 详解

1. 注解通知的类型

通知的配置语法:@通知注解(“切点表达式")

 

 2.切点表达式的抽取

        同 xml 配置 aop 一样,我们可以将切点表达式抽取。抽取方式是在切面内定义方法,在该方法上使用@Pointcut 注解定义切点表达式,然后在在增强注解中进行引用。具体如下:

 

@@Component("myAspect")
@Aspect
public class MyAspect {

@Before("MyAspect.myPoint()")
public void before(){
    System.out.println("前置代码增强.....");
    }

//只用创建一个空方法,用来写注解的
    @Pointcut("execution(* com.itheima.aop.*.*(..))")
    public void myPoint(){}
}

十、声明式事务控制

1)编程式事务控制相关对象

        编程式事务控制三大对象

                ①PlatformTransactionManager

                ②TransactionDefinition

                ③TransactionStatus

①PlatformTransactionManager

        PlatformTransactionManager 接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法。

 

        注意: PlatformTransactionManager 是接口类型,不同的 Dao 层技术则有不同的实现类,例如:

Dao 层技术是jdbc 或 mybatis 时:        org.springframework.jdbc.datasource.DataSourceTransactionManager

Dao 层技术是hibernate时:     org.springframework.orm.hibernate5.HibernateTransactionManager

 

②TransactionDefinition

TransactionDefinition 是事务的定义信息对象,里面有如下方法:

 

1.事务隔离级别

设置隔离级别,可以解决事务并发产生的问题,如脏读、不可重复读和虚读。

  • ISOLATION_DEFAULT                                     使用后端数据库默认的隔离级别
  • ISOLATION_READ_UNCOMMITTED              允许读取尚未提交的更改。可能导致脏读、但                                                                            幻影或不可重复读
  • ISOLATION_READ_COMMITTED                   从已经提交的并发事务读取
  • ISOLATION_REPEATABLE_READ                  对相同字段的多次读取的结果是一致的,除非                                                                            数据被当前事务本身改变
  • ISOLATION_SERIALIZABLE                            完全服从ACID的隔离级别,确保不发生脏                                                                                  读、不可重复读和幻读。

2.事务传播行为 

  • REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
  • MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
  • REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
  • NEVER:以非事务方式运行,如果当前存在事务,抛出异常
  • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作
  • 超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置
  • 是否只读:建议查询时设置为只读

③TransactionStatus

TransactionStatus 接口提供的是事务具体的运行状态,方法介绍如下。

 

2)基于 XML 的声明式事务控制

1.什么是声明式事务控制

        Spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明 ,用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。

        声明式事务处理的作用

  • 事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如 此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话, 也只需要在定义文件中重新配置即可
  • 在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译 ,这样维护起来极其方便

        注意:Spring 声明式事务控制底层就是AOP。

2.声明式事务控制的实现

实现步骤:

                ① 导入 AOP 相关坐标

                ② 配置目标对象(切点)

                ③ 引入tx命名空间

                ④ 配置事务增强

                ⑤ 配置事务 AOP 织入

① 导入 AOP 相关坐标

<!--导入spring的context坐标,context依赖aop-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>

② 配置目标对象(切点)

<!--目标对象  内部的方法就是切点-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"/>
    </bean>

③ 引入tx命名空间

 ④ 配置事务增强

<!--配置平台事务管理器 ,根据需要底程运用的技术的选择平台-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--    transactionManager底层需要从 dataSource拿connection进行事务控制   -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--    通知  事务的增强-->
    <!--    transaction-manager 平台事务管理器 所以需要配置-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--设置事务的属性信息的-->
        <tx:attributes>
            <!--tx:method 哪些方法被增强 name="被增强的方法名称" isolation-事务隔离级别 propagation-事务传播行为 read-only-只读属性-->
            <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="save" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="findAll" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

⑤ 配置事务 AOP 织入

 <!--配置事务的aop织入-->
    <aop:config>
        <!-- 切点表达式的提前   -->
        <aop:pointcut id="txPointcut" expression="execution(* com.itheima.service.impl.*.*(..))"/>
        <!-- 事务增强用<aop:advisor/>  advice-ref 通知引用 pointcut-ref 切点表达式-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

3.切点方法的事务参数的配置

<!--事务增强配置-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

其中,<tx:method> 代表切点方法的事务参数的配置,例如:

<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="-1" 
read-only="false"/>
  • name:切点方法名称
  • isolation:事务的隔离级别
  • propogation:事务的传播行为
  • timeout:超时时间
  • read-only:是否只读

3)基于注解的声明式事务控制

注解声明式事务控制的配置要点:

  • 平台事务管理器配置(xml方式)
  • 事务通知的配置(@Transactional注解配置)
  • 事务注解驱动的配置     
    <tx:annotation-driven transaction-manager="transactionManager"/>

1.编写 AccoutDao

@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void out(String outMan, double money) {
        jdbcTemplate.update("update account set money=money-? where name=?",money,outMan);
    }

    public void in(String inMan, double money) {
        jdbcTemplate.update("update account set money=money+? where name=?",money,inMan);
    }
}

2. 编写 AccoutService

@Service("accountService")
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public void transfer(String outMan, String inMan, double money) {
        accountDao.out(outMan,money);
        int i = 1/0;
        accountDao.in(inMan,money);
    }

    //@Transactional(isolation = Isolation.DEFAULT)
    public void xxx(){}
}

3. 编写 applicationContext.xml 配置文件

<!--加载外部的properties文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--产生数据源对象-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!--        name="填的是set后面这一些,把大写改成小写"-->
        <property name="driverClassName" value="${jdbc.driverClassName}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!--jdbc模板对象-->
    <!--JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

   <!--平台事务管理器的配置-->
 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

<!--组件扫描-->
<context:component-scan base-package="com.itheima"/>

<!--事务的注解驱动-->
<tx:annotation-driven/>

① 使用 @Transactional 在需要进行事务控制的类或是方法上修饰,注解可用的属性同 xml 配置方式,例如隔离 级别、传播行为等。

② 注解使用在类上,那么该类下的所有方法都使用同一套注解参数配置。

③ 使用在方法上,不同的方法可以采用不同的事务参数配置。

④ Xml配置文件中要开启事务的注解驱动         

<tx:annotation-driven transaction-manager="transactionManager"/>

  • 45
    点赞
  • 254
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值