spring

Spring

  • 官方文档地址:https://docs.spring.io/springframework/docs/current/reference/html/
  • 官方下载地址:https://repo.spring.io/release/org/springframework/spring/

spring 依赖

<dependency>    
	<groupId>org.springframework</groupId>    
    <artifactId>spring-webmvc</artifactId>    
    <version>5.3.6</version>
</dependency>

spring-jdbc 依赖

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

简介

  • Spring: 春天 ,给软件行业带来了春天
  • 2002 年 首次推出了 Spring 框架的雏形:interface21 框架!
  • 2004 年 3 月 24 日,Spring 框架以 interface21 框架为基础,发布了 Spring1.0 正式版
  • Rod Johnson Spring 框架的创始人
  • spring 理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架

优点

  • Spring 是一个开源的免费的框架 (容器)
  • Spring 是一个轻量级的,非入侵式的框架!本身很小,引入 Spring 不会改变原来的代码的情况,反而会简单
  • 控制反转 IOC , 面向切面编程 AOP
  • 支持事务的处理,对框架整合的支持

总结:
Spring 就是一个轻量级的控制反转 IOC 和面向切面编程 AOP 的框架

组成

七大模块

扩展

在 Spring 的官网有这个介绍,现代化的 Java 开发!说白了就是基于 Spring 的开发
img

  • Spring boot :
    是一个快速开发的脚手架
    基于 SpringBoot 可以快速的开发单个微服务
    约定大于配置
  • SpringCloud:是基于 SpringBoot 实现的

因为现在大多数公司都使用 SpringBoot 进行快速开发,学习 SpringBoot 的前提,需要完全掌握 Spring 以及 SpringMVC!承上启下的作用!

弊端:发展了太久之后,违背了原来的理念,配置十分繁琐,人称:” 配置地狱”。

IOC

理念推导

我们之前的业务中,用户的需求可能会影响我们原来代码,我们需要根据用户的需求去修改原代码,如果程序的代码量十分巨大,修改一次的成本代价十分昂贵!

原来:

private UserDao userDao=new UserDaoImpl();

我们使用一个 set 接口实现,已经发生了革命性的变化

private UserDao userDao;
//利用set进行动态实现值的注入
public void setUserDao(UserDao userDao) {  
    this.userDao = userDao;
}
  • 之前,程序是主动创建对象,控制权在程序猿手上

  • 使用了 set 注入后,程序不在具有主动权,而是变成了被动的接受对象

    这种思想,从本质上解决了问题,我们程序猿不用再去管理对象的创建了。系统的耦合性大大降低,可以更加专注的在业务的实现上,这是 IOC 的原型

本质

控制反转(IoC:Inversion of Control)不是什么技术,而是一种设计思想

DI (依赖注入) 是实现 IOC 的一种方法

也有人认为 DI 只是 IOC 的另一种说法,没有 IOC 的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓的控制反转就是:获得依赖对象的方式反转了

控制反转是一种通过描述 (XML 或者注解) 并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IOC 容器,其实现方法就是依赖注入!

HelloSpring

创建一个实体类 Hello

public class Hello { 
    
    private String name;   
    
    public String getName() {   
        return name; 
    }    
    
    public void setName(String name) {      
        this.name = name;    
    }    
    
    @Override   
    public String toString() {     
        return "Hello{" +         
            "name='" + name + '\'' +      
            '}';  
    }
}

添加 Spring 配置文件 beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"       	
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           https://www.springframework.org/schema/beans/spring-beans.xsd">   
    
    <!-- 
		使用Spring来创建对象 在Spring中这些都称为Bean
        Hello hello = new Hello()
        bean = 对象  new Hello()
        id=变量名 hello
        class=new Hello()
        property 相当于给对象中的属性设置一个值
        ref:引用Spring容器中创建好的对象id
        Values: 具体的值,基本数据类型和String 
	--> 
    
    <bean id="hello" class="com.dada.pojo.Hello">  
        <property name="name" value="Spring" />
    </bean>
    
</beans>
  • id = 变量名 hello
  • class=new Hello()
  • property 相当于给对象中的属性设置一个值
  • ref: 引用 Spring 容器中创建好的对象 id
  • Values: 具体的值,基本数据类型和 String

测试代码

@Test
public void test(){  
    
    //获取Spring的上下文对象    
    ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml");    
    //我们的对象现在都在Spring中管理了。我们要使用直接去里面取出来就可以了    
    Hello hello = (Hello) context.getBean("hello");    
    System.out.println(hello);
}

控制反转

控制:传统应用程序的对象是程序本身控制创建的,使用了 spring 后,对象是有 spring 来创建的

反转:程序本身不在创建对象,而变成被动的接收对象

依赖注入:就是利用 set 方法来进行注入的

IOC 是一种编程思想,由主动编程变成被动的接收

可以通过 new ClassPathXmlApplicationContext 查看底层源码

我们彻底不用再程序中去改动了,要实现不同的操作,只需要在 xml 配置文件中进行修改,

所谓的 IOC 一句话搞定 :对象由 Spring 来创建,管理,装配。

IOC 创建对象的方式

  1. 使用无参构造创建对象,默认实现

  2. 有参构造的创建方式

    实体类添加有参构造

public class User { 
    
    private String name; 
    
    public User(String name){    
        this.name=name;        
    }  
    
    public String getName() {    
        return name;  
    }   
    
    public void setName(String name) {  
        this.name = name;   
    }    
    
    public void show(){ 
        System.out.println("name= "+name); 
    }
    
}

Spring 配置:
第一种方式: 下标赋值

<bean id="user" class="com.dada.pojo.User"> 
    <!--第一种方式,下标赋值-->  
    <constructor-arg index="0" value="狂神说Java"/>
</bean>

第二种方式:通过类型创建,不建议使用

<bean id="user" class="com.dada.pojo.User">   
    <!--第二种方式:通过类型创建,不建议使用-->  
    <constructor-arg type="java.lang.String" value="kuangShen"/>
</bean>

第三种方式:直接通过参数名来设置

<bean id="user" class="com.dada.pojo.User">   
    <!--第三种方式:直接通过参数名设置--> 
    <constructor-arg name="name" value="张三"/>
</bean>

总结

在配置文件加载的时候,容器中管理的所有对象就已经初始化了!

Spring 配置说明

如果添加了别名,我们也可以使用别名获取到这个对象

<alias name="user" alias="userNew"/>
public void test(){ 
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");  
    //User user = (User) context.getBean("user");  
    User user = (User) context.getBean("userNew");
}

Bean 配置

  • id: bean 的唯一标识符,也就是我们的对象名
  • class: bean 对象所对应的全限定名: 包名 + 类名
  • name:别名,可以同时取多个别名
  • property 相当于给对象中的属性设置一个值
  • ref: 引用 Spring 容器中创建好的对象 id
  • Values: 具体的值,基本数据类型和 String
<bean id="user" class="com.dada.pojo.User" name="user2,u2 u3;u4">  
    <property name="name" value="Spring" />
</bean>

import

一般用于团队开发使用,可以将多个配置文件,导入合并为一个

假设项目有多个人开发,负责不同的类开发,不同的类需要注册在不同的 bean 中,我们可以利用 import 将所有人的 beans.xml 合并为一个总的配置文件,使用的时候,直接使用总的配置就可以了。

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

测试

@Test
public void test(){   
    ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");   
    User user = (User) context.getBean("user");  
    user.show();
}

DI 依赖注入

构造器注入

前面已经说了

set 方式注入【重点】

依赖注入:set 注入

  • 依赖:bean 对象的创建依赖于容器
  • 注入:bean 对象中的所有属性没,由容器来注入

【环境搭建】

  1. 真实测试对象

  2. 复杂类型

@Data
public class Student {  
    
    private String name; 
    private Address address;   
    private String[] books; 
    private List<String> hobbys;   
    private Map<String,String> card;  
    private Set<String> games; 
    private String wife;   
    private Properties info;
    
}
@Data
public class Address {   
    
    private String address;
    
}

3.beans.xml 完善注入信息

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://www.springframework.org/schema/beans          https://www.springframework.org/schema/beans/spring-beans.xsd"> 
    
    <bean id="student" class="com.dada.pojo.Student">      
        <!--第一种:普通值注入,value-->    
        <property name="name" value="张三"/>  
        <!--第二种:对象bean注入,ref引入-->      
        <property name="address" ref="address"/>  
        <!--第三种:数组注入,array-->  
        <property name="books">   
            <array>            
                <value>Java</value> 
                <value>Spring</value> 
                <value>SpringMVC</value>   
                <value>SpringBoot</value>   
            </array>        
        </property>  
        
        <!--第四种:list集合注入,list-->  
        <property name="hobbys">        
            <list>           
                <value>吃饭</value>  
                <value>睡觉</value>    
                <value>写代码</value>     
            </list>     
        </property>   
        
        <!--第五种:map注入,map entry -->    
        <property name="card">          
            <map key-type="java.lang.String" value-type="java.lang.String">  
                <entry key="1" value="阿里巴巴规范"/>     
                <entry key="2" value="数据库优化"/>        
            </map>     
        </property>   
        
        <!--第六种:set注入 --> 
        <property name="games">       
            <set>             
                <value>LOL</value>    
                <value>DNF</value>      
            </set>   
        </property>     
        
        <!--第七种:空值null注入 -->    
        <property name="wife">      
            <null/>     
        </property>  
        
        <!--第八种:Properties注入-->     
        <property name="info">         
            <props>              
                <prop key="username">root</prop>
                <prop key="password">root</prop>     
            </props>     
        </property>
        
    </bean>  
    
    <bean id="address" class="com.dada.pojo.Address">   
        <property name="address" value="杭州"/>  
    </bean>
    
</beans>

测试结果

Student(  
    name=张三,   
    address=Address(address=杭州),  
    books=[Java, Spring, SpringMVC, SpringBoot], 
    hobbys=[吃饭, 睡觉, 写代码], 
    card={1=阿里巴巴规范, 2=数据库优化},
    games=[LOL, DNF],  
    wife=null,    
    info={password=123456, username=root}
)

拓展方式注入

我们可以使用 p 命名空间和 c 命名空间进行注入

实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {    
    
    private String name;  
    private int age;
    
}

p 命名空间注入

p 命名空间注入,可以直接注入属性值:相当于 property 标签的属性注入

要在配置文件导入 xml 约束

xmlns:p="http://www.springframework.org/schema/p"

配置文件

<?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:p="http://www.springframework.org/schema/p"        xsi:schemaLocation="http://www.springframework.org/schema/beans         https://www.springframework.org/schema/beans/spring-beans.xsd">   
    <!--p命名空间注入,可以直接注入属性值:property--> 
    <bean id="user" class="com.dada.pojo.User" p:name="张三" p:age="18"/>
    
</beans>

c 命名空间注入

c 命名空间注入,构造器注入:相当于 constructor-arg 标签注入

要在配置文件导入 xml 约束

xmlns:c="http://www.springframework.org/schema/c"

配置文件

<?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:c="http://www.springframework.org/schema/c"        xsi:schemaLocation="http://www.springframework.org/schema/beans         https://www.springframework.org/schema/beans/spring-beans.xsd">   
    <!--c命名空间注入,构造器注入:constructor-arg-->  
    <bean id="user2" class="com.dada.pojo.User" c:name="李四" c:age="20"/>
</beans>

Bean 的作用域 Scopes

img

1. 单例模式 singleton (Spring 默认)

每次从容器中 getBean 都是同一个对象 单例模式只有一个

<!--显示定义单例模式-->
<bean id="user" class="com.dada.pojo.User" p:name="张三" p:age="18" scope="singleton"/>

测试结果 true

@Test
public void test2(){   
    ApplicationContext context =     
        new ClassPathXmlApplicationContext("userbean.xml");   
    User user = context.getBean("user", User.class);   
    User user2 = context.getBean("user", User.class);   
    System.out.println(user==user2);
}

2. 原型模式 prototype

每次从容器中 getBean 都会产生一个新的对象

<bean id="user" class="com.dada.pojo.User" p:name="张三" p:age="18" scope="prototype"/>

测试结果为 false

@Test
public void test2(){   
    ApplicationContext context =   
        new ClassPathXmlApplicationContext("userbean.xml"); 
    User user = context.getBean("user", User.class);  
    User user2 = context.getBean("user", User.class);  
    System.out.println(user==user2);
}

3.session,request,application

这些只能在 web 开发中使用到

Bean 的自动装配

  • 自动装配是 Spring 满足 bean 依赖一种方式
  • Spring 会在上下文中自动寻找,并自动给 bean 装配属性

三种装配的方式

  1. 在 xml 中显示的配置

  2. 在 java 中显示配置

  3. 隐式的自动装配 bean【重点掌握】

环境搭建

  • 创建实体类
  • 配置文件
  • 测试

创建 3 个实体类

import lombok.Data;

@Data
public class People {  
        
        private Cat cat;   
        private Dog dog;   
        private String name;
        
}
public class Dog {   
    
    public void shout(){    
        System.out.println("旺~");  
    }
    
}
public class Cat { 
    
    public void shout(){    
        System.out.println("喵~");   
    }
    
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"   
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       
       xsi:schemaLocation="http://www.springframework.org/schema/beans      
                           https://www.springframework.org/schema/beans/spring-beans.xsd">  
    
    <bean id="dog" class="com.dada.pojo.Dog"/>   
    <bean id="cat" class="com.dada.pojo.Cat"/>  
    <bean id="people" class="com.dada.pojo.People">  
        <property name="name" value="达达"/>      
        <property name="cat" ref="cat"/>     
        <property name="dog" ref="dog"/>  
    </bean>
    
</beans>

ByName 自动装配

byName: 会自动在容器上下文中查找,和自己对象 set 方法后面的值的首字母小写对应的 bean id

bean id 是 set 方法后面值的首字母小写

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"     
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           https://www.springframework.org/schema/beans/spring-beans.xsd">   
    
    <bean id="dog" class="com.dada.pojo.Dog"/>    
    <bean id="cat" class="com.dada.pojo.Cat"/>  
    <!--    byName:会自动在容器上下文中查找,和自己对象set方法后面的值对应的bean id    -->   
    <bean id="people" class="com.dada.pojo.People" autowire="byName">     
        <property name="name" value="达达"/>   
    </bean>
    
</beans>

byType 自动装配

byType: 会自动在容器上下文中查找,和自己对象属性类型相同的 bean

byType 的时候,需要保证所有 bean 的 class 唯一,并且这个 bean 需要和自动注入的属性的类型一致

要保证对象属性类型 bean 全局唯一

<bean class="com.dada.pojo.Dog"/>
<bean class="com.dada.pojo.Cat"/>
<!--    byType:会自动在容器上下文中查找,和自己对象属性类型相同的bean    -->
<bean id="people" class="com.dada.pojo.People" autowire="byType"> 
    <property name="name" value="达达"/>
</bean>

小结

  • byName 的时候,需要保证所有 bean 的 id 唯一,并且这个 bean 需要和自动注入的属性的 set 方法的首字母小写的值一致
  • byType 的时候,需要保证所有 bean 的 class 唯一,并且这个 bean 需要和自动注入的属性的类型一致

使用注解自动装配

jdk1.5 支持的注解,Spring2.5 就支持注解了

配置

要使用注解须知

  1. 导入约束
<?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    
                           https://www.springframework.org/schema/beans/spring-beans.xsd         
                           http://www.springframework.org/schema/context  
						   https://www.springframework.org/schema/context/spring-context.xsd">    
    <!--开启注解的支持-->   
    <context:annotation-config/>
</beans>
  1. 配置注解的支持
<context:annotation-config/>

@Autowired

自动装配:默认通过 ByType 查找

在属性上添加即可。也可以在 set 方法上使用。

@Data
public class People {   
    
    @Autowired(required = false) 
    private Cat cat;  
    @Autowired    
    private Dog dog;   
    private String name;
    
}

使用 Autowired 我们可以不用编写 set 方法了,前提是你这个自动装配的属性在 IOC (Spring) 容器中存在,且符合名字 byName

科普:

如果显示定义了 Autowired 的 required 属性为 false,说明这个对象可以为 null,否则不能为空

@Autowired(required = false)
private Cat cat;

字段标记了这个注解,说明字段可以为 null

@Nullable
private String name;

@Qualifie

如果 @Autowired 自动装配的环境比较复杂的时候,自动装配无法通过一个注解【@Autowired】完成

使用 @Qualifier(value=”xxx”) 去配合 @Autowired 的使用,指定一个唯一的 bean 对象注入

<bean id="cat1" class="com.dada.pojo.Cat"/>
<bean id="cat2" class="com.dada.pojo.Cat"/>
@Autowired
@Qualifier(value = "cat1")
private Cat cat;

@Resource

默认通过 byName 查找

<bean id="cat1" class="com.dada.pojo.Cat"/>
<bean id="cat2" class="com.dada.pojo.Cat"/>
@Resource(name = "cat2")
private Cat cat;

小结

@Autowired@Resource 的区别:

  • 都是用来自动装配,都可以放在属性字段上或写在 setter 方法上。
  • @Autowired 默认按类型装配(这个注解是属于 spring 的),默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合 @Qualifier 注解进行使用,如下:
@Autowired
@Qualifier(value = "cat1")
private Cat cat;
  • @Resource(这个注解属于 J2EE 的),默认按照名称进行装配,名称可以通过 name 属性进行指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在 setter 方法上默认取属性名进行装配。当找不到与名称匹配的 bean 时才按照类型进行装配。但是需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配。
@Resource(name = "cat2")
private Cat cat;

注解开发

在 Spring4 之后,要使用注解开发,必须要保证 aop 的包导入了

  1. 使用注解需要导入 context 约束,添加注解的支持
<?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    
                           https://www.springframework.org/schema/beans/spring-beans.xsd        
                           http://www.springframework.org/schema/context 
						   https://www.springframework.org/schema/context/spring-context.xsd">   
    <!--开启注解的支持-->   
    <context:annotation-config/>
</beans>

@Component

指定要扫描的包,这个包下的注解就会生效

    <context:component-scan base-package="com.dada.pojo"/>
@Component 
//等价于 <bean id="user" class="com.dada.pojo.User"/>
@Data
public class User {   
    
   private String name;
        
}

@Value

@Value 添加在字段上或者 set 方法上,设置属性的值

@Component 
//等价于 <bean id="user" class="com.dada.pojo.User"/>
@Data
public class User {    
    
    @Value("狂神")   
    private String name;
    
}

@Component 衍生的注解

@Component 有几个衍生注解,我们在 web 开发中,会按照 mvc 三层架构分层

这 4 个注解功能都是一样的,都是代表某个类注册到 Spring 容器中,自动装配

@Scope 作用域

@Scope(“singleton”)

添加到类上 设置为单例模式

小结

xml 与注解:

  • xml 更加万能,适用于任何场合,维护简单方便
  • 注解 不是自己类使用不了。维护相对复杂

xml 与注解 最佳实践:

  • xml 用来管理 bean
  • 注解只负责完成属性的注入

我们在使用的过程中,只需要注意一个问题,必须让注解生效就需要开启注解的支持

<!--开启注解的支持-->
<context:annotation-config/>
<!--指定要扫描的包,这个包下的注解就会生效-->
<context:component-scan base-package="com.dada.*"/>

使用 Java 的方式配置 Spring

我们现在要完全不使用 Spring 的 xml 配置了,全权交给 Java 来做

JavaConfig 是 Spring 的一个子项目,在 Spring4 之后,它成为了一个核心功能!

需要使用 AnnotationConfigApplicationContext () 加载配置类获取容器

ApplicationContext context =new AnnotationConfigApplicationContext(MyConfig.class)

1. 配置文件类

//这个也会被spring容器托管,也会被注册到容器中,因为他本来就是一个
@Component
//@Configuration代表是一个配置类和Spring.xml一样
@Configuration
@ComponentScan("com.dada") 
//扫描包
public class MyConfig {  
    
    /*    注册一个bean 相当于bean标签  
    id相当于方法名  
    class相当于返回值  
    return 就是返回要注入bean的对象  
    */    
    
    @Bean  
    public User user(){     
        
        return new User();  
        
    }
    
}

2. 实体类

@Data
@Component 
//说明这个类被spring接管了,注册到了容器中
public class User {   
    
    @Value("zhangsan")
    //属性注入值   
    private String name;

}

3. 测试

public class MyTest {  
    
    @Test    
    public void test(){     
        //使用配置类加载获取spring容器 
        ApplicationContext context =    
            new AnnotationConfigApplicationContext(MyConfig.class);  
        User getUser = context.getBean("user", User.class);      
        System.out.println(getUser); 
    }
    
}

这种纯 Java 配置方式,在 Springboot 中随处可见!

代理模式

为什么要学习代理模式?

因为这就是 SpringAOP 的底层!

面试: 【SpringAOP 和 SpringMVC】

静态代理

角色分析:

  • 抽象的角色:一般用接口或者抽象类解决 - — 租房
  • 真实的角色:被代理的角色 —- 房东
  • 代理角色:代理真实角色,代理真实角色后,我们一般做一些附属操作 — 中介
  • 客户:访问代理对象的人 — 我

代码步骤

  1. 接口
//租房
public interface Rent {
    void rent();
}
  1. 真实角色
//房东
public class Host implements Rent{ 
    
    public void rent() {     
        System.out.println("房东要出租房子!!");  
    }
    
}
  1. 代理角色
//中介
public class Proxy implements Rent{  
    
    private Host host;   
    
    public Proxy() {  
    }    
    
    public Proxy(Host host) {   
        this.host = host; 
    }   
    
    public void rent() {    
        seeHouse();        
        host.rent();  
        //代理租房     
        heTong();   
    }    
    
    //看房    
    public void seeHouse(){ 
        System.out.println("中介带领看房!");
    }   
    
    //签合同 
    public void heTong(){   
        System.out.println("中介和你签合同!");  
    }  
    
}
  1. 客户端访问代理角色
//客户
public class Client { 
    
    public static void main(String[] args) {
        Host host=new Host();    
        Proxy proxy = new Proxy(host);   
        proxy.rent(); 
    }
    
}

静态代理的好处

  • 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
  • 公共的业务就交给代理角色!实现了业务的分工!
  • 公共业务发生扩展的时候,方便集中管理
练习代理

接口

public interface UserService {  
    
    //增加一个用户   
    void add(); 
    //删除一个用户
    void delete();
    //修改一个用户  
    void update();  
    //查询一个用户  
    void query();
    
}

真实角色

public class UserServiceImpl implements UserService{  
    
    public void add() {    
        System.out.println("增加了一个用户!");  
    }  
    
    public void delete() {   
        System.out.println("删除了一个用户!");  
    }   
    
    public void update() {    
        System.out.println("修改了一个用户!");  
    }   
    
    public void query() {    
        System.out.println("查询了一个用户!");  
    }
    
}

代理角色

public class UserServiceImplProxy implements UserService{  
    
    UserServiceImpl userService;  
    
    public void setUserService(UserServiceImpl userService) { 
        this.userService = userService;    
    }   
    
    public void add() {    
        log("add");    
        System.out.println("增加了一个用户!");    
    }   
    
    public void delete() {  
        log("delete");      
        System.out.println("删除了一个用户!");   
    }   
    
    public void update() {        
        log("update");
        System.out.println("修改了一个用户!");
    }   
    
    public void query() {   
        log("query");     
        System.out.println("查询了一个用户!"); 
    }  
    
    //新增一个日志功能 
    public void log(String msg){ 
        System.out.println("使用了"+msg+"方法");
    }
    
}

客户端

public class Client {
    
    public static void main(String[] args) { 
        
        UserServiceImpl userService = new UserServiceImpl();    
        UserServiceImplProxy serviceImplProxy = new UserServiceImplProxy();     
        serviceImplProxy.setUserService(userService);    
        serviceImplProxy.add();       
        serviceImplProxy.delete();   
        serviceImplProxy.update();   
        serviceImplProxy.query();    
        
    }
    
}

初识 aop

img

动态代理

  • 动态代理和静态代理角色一样
  • 动态代理类是动态生成的,不是我们直接写好的
  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
    • 基于接口 —-JDK 动态代理【我们在这里使用】
    • 基于类:cglib
    • java 字节码实现:javasist

需要了解两个类:
Proxy:代理,
InvocationHandler:调用处理程序

注意点

动态代理代理的是接口

Proxy

Proxy 提供了创建动态代理类的静态方法

public static Object newProxyInstance(ClassLoader loader,        
                                      Class<?>[] interfaces,     
                                      InvocationHandler h)    throws IllegalArgumentException 
    返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
    参数
    loader - 类加载器来定义代理类
    interfaces - 代理类实现的接口列表
    h - 调度方法调用的调用处理函数
    结果 
    具有由指定的类加载器定义并实现指定接口的代理类的指定调用处理程序的代理实例

invocationHandler 调用处理程序

Object invoke(Object proxy,     
              Method method,           
              Object[] args) throws Throwable
    处理代理实例上的方法调用并返回结果。
    当在与之关联的代理实例上调用方法时,将在调用处理程序中调用此方法。 
    参数 
    proxy - 调用该方法的代理实例
    method -所述方法对应于调用代理实例上的接口方法的实例。
    方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。
    args -包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。
    原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer或java.lang.Boolean 。
    结果 
    从代理实例上的方法调用返回的值。
    如果接口方法的声明返回类型是原始类型,则此方法返回的值必须是对应的基本包装类的实例; 
	否则,它必须是可声明返回类型的类型。 
    如果此方法返回的值是null和接口方法的返回类型是基本类型,那么NullPointerException将由代理实例的方法调用抛出。 
    如上所述,如果此方法返回的值,否则不会与接口方法的声明的返回类型兼容,一个ClassCastException将代理实例的方法调用将抛出。

自动生成动态代理类的工具类

//这个类用于自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {   
    //被代理的接口 
    private Object target;
    
    public void setTarget(Object target) { 
        this.target = target;  
    }   
    
    //生成代理类    
    public Object getProxy(){  
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),              
                                      target.getClass().getInterfaces(), this);   
    }   
    
    //处理代理实例,并返回结果  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    
        Object result = method.invoke(target, args);    
        return result;  
    }
    
}

测试

public class Client {  
    public static void main(String[] args) { 
        //真实角色    
        Host host=new Host();  
        //代理角色        
        ProxyInvocationHandler pih = new ProxyInvocationHandler();    
        //通过调用程序处理角色来处理我们要调用的接口对象 
        pih.setTarget(host); 
        //设置要代理的对象    
        //动态代理代理的是接口 :注意点    
        Rent proxy = (Rent) pih.getProxy();      
        proxy.rent();  
    }
}

代理的好处

  • 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务

  • 公共的业务就交给代理角色!实现了业务的分工!

  • 公共业务发生扩展的时候,方便集中管理

  • 一个动态代理类代理的是一个接口,一般就是对应的一类业务

  • 一个动态代理类可以代理多个类,只要是实现了多个接口即可

  • AOP

    AOP(Aspect-Oriented Programming,面向方面编程),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离。从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

    AOP 的作用

    提供了声明式事务;允许用户自定义切面

    • 横切关注点:跨越应用程序多个模块的方法或者功能。即是,与我们业务逻辑无关的,但我们需要关注的部分,就是横切关注点,如日志,安全,缓存,事务等等。。。
    • 切面 (aspect): 横切关注点被模块化的特殊对象。即,它是一个类
    • 通知 (Advice):切面必须要完成的工作。即,它是类中的一个方法
    • 目标 (Target):被通知的对象
    • 代理 (Proxy):向目标对象应用通知之后创建的对象
    • 切入点 (PointCut):切面通知执行的” 地点” 的定义
    • 连接点 (JoinPoint):与切入点匹配的执行点

    SpringAOP 中,通过 Advice 定义横切逻辑,Spring 中支持 5 种类型的 Advice

    img

    即在 AOP 不改变原有代码的情况下去添加新的功能

    使用 Spring 实现 AOP【重点】

    使用 AOP 织入,需要导入一个依赖包

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>   
    <groupId>org.aspectj</groupId>  
    <artifactId>aspectjweaver</artifactId>  
    <version>1.9.6</version>
</dependency>

AOP 方式一: Spring 的 API 接口实现

接口

public interface UserService { 
  
  void add();  
  
  void delete(); 
  
  void update();   
  
  void select();
  
}

接口实现类

public class UserServiceImpl implements UserService{  
  
  public void add() {     
      System.out.println("增加了一个用户!");   
  }    
  
  public void delete() {   
      System.out.println("删除了一个用户!");  
  }   
  
  public void update() {   
      System.out.println("修改了一个用户!");   
  }  
  
  public void select() {    
      System.out.println("查询了一个用户!");  
  }
  
}

日志工具类
方法前

public class Log implements MethodBeforeAdvice{  
  /*      
  method:要执行的目标对象的方法   
  args: 参数      
  target: 目标对象  
  */  
  public void before(Method method, Object[] args, Object target) throws Throwable {       
      System.out.println(target.getClass().getName()+"执行了: "+method.getName()+"方法,参数为: "+args);    
  }
  
}

方法后

public class AfterLog implements AfterReturningAdvice { 
  /*       
  returnValue: 返回的结果  
  method:要执行的目标对象的方法    
  args: 参数        
  target: 目标对象    
  */  
  public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {     
      
      System.out.println("执行了: "+method.getName()+"方法,返回了: "+returnValue);  
  }
  
}

配置文件

<?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:aop="http://www.springframework.org/schema/aop"        xsi:schemaLocation="http://www.springframework.org/schema/beans                            https://www.springframework.org/schema/beans/spring-beans.xsd                             http://www.springframework.org/schema/aop                            http://www.springframework.org/schema/aop/spring-aop.xsd"> 
  
  <bean id="userService" class="com.dada.service.UserServiceImpl"/>  
  <bean id="log" class="com.dada.log.Log"/>  
  <bean id="afterLog" class="com.dada.log.AfterLog"/>
  <!--方式一: 使用Spring API接口配置aop需要:导入aop的约束  
  		    xmlns:aop="http://www.springframework.org/schema/aop" 
  			http://www.springframework.org/schema/aop
  			http://www.springframework.org/schema/aop/spring-aop.xsd 
  -->   
  
  <aop:config>   
      <!--  
    	切入点:pointcut
      要执行的位置: expression()表达式 * * * * * 修饰词 返回值 类名 方法名 参数   
   	execution(* com.dada.service.UserServiceImpl.*(..)  UserServiceImpl类的任意方法,任意的参数 
      -->      
      <aop:pointcut id="piontcut" expression="execution(* com.dada.service.UserServiceImpl.*(..) )"/> 
      <!--执行环绕增强!-->   
      <aop:advisor advice-ref="log" pointcut-ref="piontcut"/>    
      <aop:advisor advice-ref="afterLog" pointcut-ref="piontcut"/>
  </aop:config>
  
</beans>

测试代码

@Test
public void test(){  
  
  ApplicationContext context =  
      new ClassPathXmlApplicationContext("applicationContext.xml");    
  //动态代理代理的是接口   
  UserService userService = context.getBean("userService", UserService.class); 
  userService.add();
  
}

测试结果

com.dada.service.UserServiceImpl执行了: add方法,参数为: [Ljava.lang.Object;
@2145b572增加了一个用户!执行了: add方法,返回了: null

AOP 方式二:自定义切面

自定义类

public class DiyPointCut {   
  
  public void before(){        
      System.out.println("=====方法执行前=====");   
  }   
  
  public void after(){    
      System.out.println("=====方法执行后====="); 
  }
  
}

配置 xml

<!--方式二 自定义类--><bean id="diy" class="com.dada.diy.DiyPointCut"/><aop:config> 
  
  <!--自定义切面,ref引用的类--> 
  <aop:aspect ref="diy">    
      <!--切入点-->   
      <aop:pointcut id="pointcut" expression="execution(* com.dada.service.UserServiceImpl.*(..))"/>  
      <!--通知-->       
      <aop:before method="before" pointcut-ref="pointcut"/>     
      <aop:after method="after" pointcut-ref="pointcut"/> 
  </aop:aspect>
  
</aop:config>

测试

@Test
public void test(){ 
  ApplicationContext context =      
      new ClassPathXmlApplicationContext("applicationContext.xml");  
  //动态代理代理的是接口 
  UserService userService = context.getBean("userService", UserService.class);
  userService.add();
}

测试结果

=====方法执行前=====增加了一个用户!=====方法执行后=====

AOP 方式三:注解实现

@Aspect : 标记一个类为切面

自定义类

@Aspect 
//标记这个类为切面
@Component
public class AnnotationPointCut {  
  
  @Before("execution(* com.dada.service.UserServiceImpl.*(..))")   
  public void before(){     
      System.out.println("=====方法执行前=====");  
  }    
  
  @After("execution(* com.dada.service.UserServiceImpl.*(..))")   
  public void after(){    
      System.out.println("=====方法执行后=====");    
  }   
  
  //在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点    
  @Around("execution(* com.dada.service.UserServiceImpl.*(..))")  
  public void around(ProceedingJoinPoint jp) throws Throwable {   
      System.out.println("=========环绕前========");     
      //执行方法     
      Object proceed = jp.proceed(); 
      System.out.println("=========环绕后========");  
  }
  
}

配置 xml

<?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:aop="http://www.springframework.org/schema/aop"        xmlns:context="http://www.springframework.org/schema/context"        xsi:schemaLocation="http://www.springframework.org/schema/beans        https://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/aop         
http://www.springframework.org/schema/aop/spring-aop.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context.xsd">   
  <context:component-scan base-package="com.dada"/>   
  <!--方式三: 注解-->  
  <!--开启注解支持-->  
  <aop:aspectj-autoproxy/>
</beans>

测试结果

=========环绕前=============方法执行前=====增加了一个用户!=====方法执行后==============环绕后========

Spring 整合 Mybatis

导入相关的 jar 包

  • junit
<dependency> 
    <groupId>junit</groupId>  
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>
  • mybatis
<dependency>  
    <groupId>org.mybatis</groupId>  
    <artifactId>mybatis</artifactId> 
    <version>3.5.6</version>
</dependency>
  • mysql 数据库
<dependency>  
    <groupId>mysql</groupId>    
    <artifactId>mysql-connector-java</artifactId>  
    <version>5.1.47</version>
</dependency>
  • spring 相关的
<dependency>   
    <groupId>org.springframework</groupId>  
    <artifactId>spring-webmvc</artifactId>  
    <version>5.3.6</version>
</dependency>
  • aop 织入
<dependency>   
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId> 
    <version>1.9.6</version>
</dependency>
  • mybatis-spring
<dependency>  
    <groupId>org.mybatis</groupId>   
    <artifactId>mybatis-spring</artifactId> 
    <version>2.0.6</version>
</dependency>
  • jdbc
<dependency>   
    <groupId>org.springframework</groupId> 
    <artifactId>spring-jdbc</artifactId>   
    <version>5.3.6</version>
</dependency>
  • lombok
<dependency>    
    <groupId>org.projectlombok</groupId> 
    <artifactId>lombok</artifactId>  
    <version>1.18.12</version>
</dependency>

回忆 mybatis

  1. 编写实体类
@Data
public class User {   
    
    private int id;   
    private String name; 
    private String pwd;
    
}
  1. 编写核心配置文件
<?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核心配置文件-->
<configuration>  
    
    <!--引入外部配置文件-->
    <properties resource="db.properties" />   
    
    <typeAliases>      
        <package name="com.dada.pojo"/> 
    </typeAliases>   
    
    <environments default="development">  
        <environment id="development">   
            <transactionManager type="JDBC"/>    
            <dataSource type="POOLED">     
                <property name="driver" value="${driver}"/>          
                <property name="url" value="${url}"/>          
                <property name="username" value="${username}"/>    
                <property name="password" value="${password}"/>    
            </dataSource>    
        </environment>   
    </environments> 
    
    <!--  每一个mapper.xml都需要在mybatis核心配置文件中注册  -->  
    <mappers>   
        <mapper class="com.dada.mapper.UserMapper"/> 
    </mappers>
    
</configuration>
  1. 编写接口
public interface UserMapper {
    List<User> selectUser();
}
  1. 编写 Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper      
  PUBLIC "-//mybatis.org//mapper Config 3.0//EN"       
     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dada.mapper.UserMapper">  
    
    <select id="selectUser" resultType="user">   
        select * 
        from mybatis.user
    </select>
    
</mapper>
  1. 测试
  @Test
public void selectUser() throws IOException { 
    
    String resource="mybatis-config.xml"; 
    InputStream in = Resources.getResourceAsStream(resource);  
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);  
    SqlSession sqlSession = factory.openSession(true);
    
    //开启自动提交事务  
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);   
    List<User> userList = mapper.selectUser(); 
    for (User user : userList) {  
        System.out.println(user);  
    }    
    
    sqlSession.close();
    
}

Mybatis-Spring

步骤:

1. 在 spring-dao.xml 配置中编写数据源配置 DriverManagerDataSource

使用 Spring 的数据源代替 Mybatis 的配置

<!--DataSource:使用Spring的数据源代替Mybatis的配置 c3p0 dbcp druid      
  这里使用Spring提供的JDBC:org.springframework.jdbc.datasource.DriverManagerDataSource 
-->
<context:property-placeholder ignore-unresolvable="true" location="classpath:dataBase.properties"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</bean>

2. 配置 sqlSessionFactory

<!--sqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 
    <property name="dataSource" ref="dataSource"/>   
    <!--绑定mybatis配置文件-->   
    <property name="configLocation" value="classpath:mybatis-config.xml"/> 
    <property name="mapperLocations" value="classpath:com/neu/mapper/*.xml"/>
</bean>

3. 配置 SqlSessionTemplate

SqlSessionTemplate 就是我们使用的 SqlSession

<!--  
  SqlSessionTemplate就是我们使用的SqlSession 
  只能用构造器注入,因为没有set方法   
-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">   
    <constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>

4. 需要给接口添加实现类

把之前 mybatis 做的事情交给 spring 来做

public class UserMapperImpl implements UserMapper{  
    //原来我们的所有都是使用SqlSession操作,现在使用SqlSessionTemplate   
    private SqlSessionTemplate sqlSession;  
    
    public void setSqlSession(SqlSessionTemplate sqlSession) { 
        this.sqlSession = sqlSession;    
    }   
    
    public List<User> selectUser() {   
        return sqlSession.getMapper(UserMapper.class).selectUser();
    }
    
}

5.applicationContext.xml 注册实现类

<?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:aop="http://www.springframework.org/schema/aop"       
       xmlns:context="http://www.springframework.org/schema/context"       
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           https://www.springframework.org/schema/beans/spring-beans.xsd      
                           http://www.springframework.org/schema/aop         
						   http://www.springframework.org/schema/aop/spring-aop.xsd         
                           http://www.springframework.org/schema/context         
						   http://www.springframework.org/schema/context/spring-context.xsd">    
    
    <import resource="spring-dao.xml"/>   
    
    <!--注册UserMapperImpl 注入属性-->  
    <bean id="userMapper" class="com.dada.mapper.UserMapperImpl"> 
        <property name="sqlSession" ref="sqlSession"/>   
    </bean>
    
</beans>

6. 测试

@Test
public void selectUser() throws IOException {   
    ApplicationContext context =     
        new ClassPathXmlApplicationContext("applicationContext.xml");
    UserMapper userMapper = context.getBean("userMapper", UserMapper.class); 
    
    for (User user : userMapper.selectUser()) {  
        System.out.println(user);
    }
}

7. 方式二

实现类 继承了 SqlSessionDaoSupport

public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper{ 
    
    public List<User> selectUser() {   
        return getSqlSession().getMapper(UserMapper.class).selectUser();    
    }
    
}

配置文件 applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:aop="http://www.springframework.org/schema/aop"        xmlns:context="http://www.springframework.org/schema/context"        xsi:schemaLocation="http://www.springframework.org/schema/beans        https://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/aop         
http://www.springframework.org/schema/aop/spring-aop.xsd         http://www.springframework.org/schema/context         
http://www.springframework.org/schema/context/spring-context.xsd"> 
    
    <import resource="spring-dao.xml"/>  
    
    <!-- 注入属性 sqlSessionFactory -->   
    <bean id="userMapper2" class="com.dada.mapper.UserMapperImpl2">  
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/> 
    </bean>
    
</beans>

声明式事务

transction

回顾事务:

  • 要么都成功,要么都失败
  • 事务在项目开发中,十分的重要,涉及到数据的一致性问题,不能马虎
  • 确保完整性和一致性

事务的 ACID 原则:

  • 原子性:确保要么都成功,要么都失败
  • 一致性:操作前和操作后的数据一致
  • 隔离性:多个业务可能同时操作一个资源,互不干扰,防止数据的损坏
  • 持久性:一旦事务被提交,就会持久化到了数据库,不可改变

声明式事务: AOP

在 xml 导入事务的约束

xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/tx
					http://www.springframework.org/schema/tx/spring-tx.xsd">

配置文件

<!--  配置声明式事务  -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--  结合aop实现事务的织入  -->
<!--  配置事务通知  -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">   
    <!-- 给方法配置事务  -->
    <!-- 配置事务的传播性  propagation="REQUIRED" 默认 -->   
    <tx:attributes>     
        <tx:method name="*" propagation="REQUIRED"/> 
    </tx:attributes>
</tx:advice>

<!-- 配置事务的切入  -->
<!-- execution(* com.dada.mapper.*.*(..)) com.dada.mapper里的全部类和全部方法  -->
<aop:config>   
    <aop:pointcut id="txPointCut" expression="execution(* com.dada.mapper.*.*(..))"/>  
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值