Spring知识大总结

2 篇文章 1 订阅

Spring

文章目录

一、Spring简介

Spring:

  • Spring是一个开源框架,它由[Rod Johnson](https://baike.baidu.com/item/Rod Johnson)创建。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。

  • Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。

  • 轻量——从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。

  • 控制反转——Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

  • 面向切面——Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

  • 容器——Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。

  • 框架——Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。

  • 所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。

二、SpringIOC

1、导入依赖

<dependencies>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>


</dependencies>

2、编写配置文件

  • 使用工厂对象获取目标对象(静态方式和动态方式)

  • 注意:

​ 若存在多个Spring配置文件,则必须要在spring主配置文件中(applicationContext.xml)导入其他配置文件,因为spring只会自动读取主配置文件。使用标签

<import resource="applicationContext_order.xml">
  • 编写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">
    <!--使用此种模式必须要存在空参构造方法-->
    <!--
		声明我们需要创建的类的对象
		在声明对象的时候,id属性也可以用name属性代替。
		id和name属性在spring3.1之后完全一样,在3.1之前,id不能以"/"开头,但是name可以
	-->
    <bean class="com.ujiuye.spring.demo1.User" id="user"></bean>

    <!--实例工厂方式获取工厂对象-->
    <bean id="factory" class="com.ujiuye.spring.demo1.Factory"></bean>
    <!--根据工厂类的对象获取User对象-->
    <bean id="user2" class="com.ujiuye.spring.demo1.User" factory-bean="factory" factory-method="getUser"></bean>

    <!--静态工厂方式获取工厂对象-->
    <bean id="user3" class="com.ujiuye.spring.demo1.Factory" factory-method="getStaticUser"></bean>
</beans>

2.1、构造器注入以及Setter注入

<?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">
    <!--属性赋值:构造方法-->
    <bean id="student1" class="com.ujiuye.spring.demo2.Student">
        <constructor-arg value="3" index="0"></constructor-arg>
        <constructor-arg value="李四" index="1"></constructor-arg>
    </bean>
    <!--根据参数类型进行匹配-->
    <bean id="student2" class="com.ujiuye.spring.demo2.Student">
        <constructor-arg type="java.lang.Integer" value="100"></constructor-arg>
        <constructor-arg type="java.lang.String" value="小光"></constructor-arg>
    </bean>
    <!--
		根据参数名称进行匹配
		注意name的值是和形参保持一致,而并不是和类中属性名保持一致
	-->
    <bean id="student3" class="com.ujiuye.spring.demo2.Student">
        <constructor-arg name="sid" value="90"></constructor-arg>
        <constructor-arg name="sname" value="小黑"></constructor-arg>
    </bean>

    <!--setter方法赋值-->
    <bean id="student4" class="com.ujiuye.spring.demo2.Student">
        <property name="sid" value="199"/>
        <property name="sname" value="小强"/>
    </bean>

    <!--复合对象:分离方式+集合属性-->
    <bean id="book" class="com.ujiuye.spring.demo2.Book">
        <property name="name" value="斗破苍穹"/>
    </bean>
    <bean id="student5" class="com.ujiuye.spring.demo2.Student2">
        <property name="sid" value="199"/>
        <property name="sname" value="小灰"/>
        <property name="book" ref="book"/>
        <property name="hobby">
            <list>
                <value>吃饭</value>
                <value>睡觉</value>
                <value>打游戏</value>
            </list>
        </property>
        <property name="scope">
            <map>
                <entry key="语文" value="148"/>
                <entry key="数学" value="150"/>
                <entry key="英语" value="145"/>
                <entry key="理综" value="280"/>
            </map>
        </property>
    </bean>

    <!--复合对象:整合方式-->
    <bean id="student6" class="com.ujiuye.spring.demo2.Student2">
        <property name="sid" value="199"/>
        <property name="sname" value="小灰"/>
        <property name="book">
            <bean class="com.ujiuye.spring.demo2.Book">
                <property name="name" value="小白"/>
            </bean>
        </property>
        <property name="hobby">
            <list>
                <value>吃饭</value>
                <value>睡觉</value>
                <value>学习</value>
            </list>
        </property>
        <property name="scope">
            <map>
                <entry key="语文" value="148"/>
                <entry key="数学" value="150"/>
                <entry key="英语" value="145"/>
                <entry key="理综" value="280"/>
            </map>
        </property>
    </bean>


    <!--标签方式赋值-->
    <bean id="student7" class="com.ujiuye.spring.demo2.Student" p:sid="10" p:sname="小黄">

    </bean>

    <!--自动装配,自动赋值-->
    <!--<bean id="money" class="com.ujiuye.spring.demo2.Money"></bean>-->
    <!--<bean id="user" class="com.ujiuye.spring.demo2.User" autowire="byType">-->

    <!--当要使用autowire="byName"的时候需要是的前面Money的bean标签的id和
        javaBean类中的成员变量名相同
    -->
    <bean id="money" class="com.ujiuye.spring.demo2.Money"></bean>
    <bean id="user" class="com.ujiuye.spring.demo2.User" autowire="byName">

    </bean>
</beans>

3、测试类

ApplicationContext context = new
        ClassPathXmlApplicationContext("demo2.xml");
User user = context.getBean("user", User.class);
System.out.println(user);

三种测试方式

//加载配置文件,从而获取spring的上下文对象(配置文件对应的对象)
ClassPathXmlApplicationContext context =
        new ClassPathXmlApplicationContext("applicationContext.xml");
//根据之前做配置文件中我们指定的ID,获取User对象

//第一种获取方式,此种模式需要进行强制转换,所以存在安全问题,不推荐使用
Object user = context.getBean("user");
System.out.println(user);

/*
    第二种获取方式
    要求该容器中只能有一个该类型的对象,即使id不同也不行
*/
User user2 = context.getBean(User.class);
System.out.println(user2);
/*
    第三种获取方式
    推荐使用方式
 */
User user3 = context.getBean("user", User.class);
System.out.println(user3);

4、关于CGLIB

​ 参考博客:

【https://blog.csdn.net/gyshun/article/details/81000997】

5、bean的生命周期

执行顺序:

  • 构造方法->初始化方法->其他方法->销毁方法(容器关闭时才会销毁-可以通过手动关闭实现context.close())

  • 其中初始化方法和销毁方法都可以在Spring容器中进行配置,实例化方法会在加载spring容器的时候自动执行。

  • 但是注意:当定义为多例的时候,即使在容器中配置了销毁方法,Spring也不会执行。

6、※spring的作用域

默认情况下, Spring 只为每个在 IOC 容器里声明的 Bean 创建唯一一个实例(单例模式), 整个 IOC 容器范围内都能共享该实例:所有后续的 getBean() 调用和 Bean 引用都将返回这个唯一的 Bean 实例.该作用域被称为 singleton, 它是所有 Bean 的默认作用域。

但是下面代码能够使得其每次返回一个新的对象:

<bean id="" class="" scope="prototype"></bean>

Bean 的作用域类型:

类型说明
singleton在 Spring 容器中仅存在一个 Bean 实例, Bean 以单例的形式存在。(IoC容器的默认值)
prototype每次从容器中调用 Bean 时,都会返回一个新的实例,即相当于执行 new XxxBean() 的实例化操作。
request每次 http 请求都会创建一个新的 Bean , 仅用于 WebApplicationContext 环境。
session同一个 http Session 共享一个 Bean ,不同的 http Session 使用不同的 Bean,仅用于 WebApplicationContext 环境。
globalSession同一个全局 Session 共享一个 bean, 用于 Porlet, 仅用于 WebApplication 环境。

7、可重复使用的注入

<!--可重复运用的注入-->
<util:list id="boddys">
    <value>小黑</value>
    <value>小白</value>
    <value>小红</value>
    <value>小蓝</value>
</util:list>
<bean id="user2" class="com.ujiuye.mybatis.bean.demo1.User">
    <property name="hobby" ref="boddys"/>
</bean>

8、级联属性赋值

<bean id="person" class="com.offcn.domain.Person">
    <property name="name" value="小强"/>
    <property name="age" value="18"/>
    <property name="address" >
        <bean class="com.offcn.domain.Address"/>
    </property>
    <property name="address.city" value="北京"/>
</bean>

9、继承Bean配置

​ Spring 允许继承 bean 的配置, 被继承的 bean 称为父 bean. 继承这个父 Bean 的 Bean 称为子 Bean

子 Bean 从父 Bean 中继承配置, 包括 Bean 的属性配置,子 Bean 也可以覆盖从父 Bean 继承过来的配置

父 Bean 可以作为配置模板, 也可以作为 Bean 实例. 若只想把父 Bean 作为模板, 可以设置 <bean> 的abstract 属性为 true, 这样 Spring 将不会实例化这个 Bean并不是 <bean> 元素里的所有属性都会被继承. 比如: autowire, abstract 等.

也可以忽略父 Bean 的 class 属性, 让子 Bean 指定自己的类, 而共享相同的属性配置. 但此时 abstract 必须设为 true

<bean id="person1" abstract="true">
        <property name="name" value="萧蔷"/>
        <property name="age" value="18"/>
    </bean>

<bean id="person2" class="com.offcn.domain.Person"  parent="person1"/>

10、SpEL(Spring 表达式语言)

表达式:操作数和运算符组成复合一定语法规则的序列

是一个支持运行时查询和操作对象图的强大的表达式语言。

语法类似于 EL:SpEL 使用 #{…} 作为定界符,所有在大框号中的字符都将被认为是 SpEL

<bean class="com.offcn.domain.Person" id="person3">
    <property name="age" value="#{18}"/>
    <!--引用其他Bean中的属性-->
    <property name="name" value="#{person2.name}"/>

    <property name="car">
        <bean class="com.offcn.domain.Car">
            <!--调用方法-->
            <property name="name" value="#{'fute'.toUpperCase()}"/>
            <!--通过 T() 调用一个类的静态方法,它将返回一个 Class Object,然后再调用相应的方法或属性-->
            <property name="area" value="#{T(java.lang.Math).PI}"/>
        </bean>

    </property>

    <property name="address">
        <bean class="com.offcn.domain.Address">
            <!-- 做判断-->
            <property name="city" value="#{address.city==null ? '北京':'上海'}"/>
        </bean>
    </property>

</bean>

小结

  • String可以使用单引号或者双引号作为字符串的定界符号
  • spel还支持一些运算:算数运算符、比较运算符( <, >, ==, <=, >=, lt, gt, eq, le, ge)、逻辑运算符号(and, or, not)

11、Spring整合Junit

注意:Junit的版本要求需要是4.10及以上版本

  • 配置之后不需要我们去启动容器

  • 测试哪个对象直接可以注入(使用注解)

  • ** 添加依赖**

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!--Spring基础依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
<!-- 整合Junit -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.6.RELEASE</version>
    <scope>test</scope>
</dependency>
  • 编写配置
// 使用Spring自定义的junit运行器替换它默认的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// 指定配置文件或者配置类
@ContextConfiguration("classpath:demo2.xml")
@Slf4j
public class Demo1 {

    @Autowired
    private User user;

    @Test
    public void demo1(){
      log.info("{}",user);
    }
}

12、依赖注入(DI思想)

注意:使用属性注入需要先给属性设置一个Setter方法

@Data
public class HelloWord{
	private String name;
}
<bean class = "上面java类全限定名" id="helloWord">
	<property name="name" value="rose">
</bean>
  • 设置之后在测试类中可以直接调用getName()方法获取到name的值。

  • 注意在Spring中若配置了属性类型为Data类型,则在容器中用标签传值的时候需要使用"/"才能被读取。

13、Spring测试

  • 使用步骤

首先导入依赖

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>5.0.0.RELEASE</version>
	<scope>test</scope>
</dependency>

使用时需要在测试方法上面添加两个注解

@RunWith(SpringJUnit4ClassRunner.class)
//表示当前测试类以Spring测试方法运行
@ContextConfiguration(value = "classpath:applicationContext.xml")
/*
	创建Spring容器的时候,使用的配置文件
	当不显示制定Spring的配置文件时,ContextConfiguration注解会寻找默认的配置文件,默认的配置文件为:测试类同一个文件夹之下,名字叫做:当前测试类类名-context.xml
*/

14、Spring创建对象的方式

  • 构造器实例化(无参数构造器,与构造器的访问权限无关),最标准,使用最多。
  • 静态工厂方法实例化
  • 实例工厂方法实例化
  • 实现FactoryBean接口实例化:实例工厂变种:集成其他框架使用:SqlSessionFactoryBean

对于前三种方式比较简单,不再进行说明,现在对第四种方式进行详细介绍,第四种方式使用的也比较多,也要求掌握:

//工厂设置
public class Dog4Factory implements FactoryBean{
    public Dog4 getObject(){
        System.out.println("实现FactoryBean接口创建Dog4对象");
        return new Dog4();
    }

    public Class<?> getObjectType() {
        return Dog4.class;
    }
}
<!--xml配置-->
<!--实现FactoryBean接口实例化-->
<bean id="dog4" class="com.ujiuye.spring._02_ioc.factorybean.Dog4Factory" />

<!--
注意:
	<bean id=”xxx” class=”xxx.xxx.xxx.XXX”>,Spring容器会自动的判断XXX类是否实现了FactoryBean接口,假如该类没有实现FactoryBean接口,那么创建出来的对象格式XXX类的对象,假如该类实现了FactoryBean接口,那么Spring创建的对象时XXX类中getObject方法返回的对象。
-->
//测试类
@RunWith(SpringJUnit4ClassRunner.class)
// 使用Spring自定义的junit运行器替换它默认的运行器
@ContextConfiguration
// 指定配置文件或者配置类,此处配置文件和当前测试类在同一级目录,可以不进行配置,默认扫描
public class App {

    @Autowired
    private ApplicationContext context;
    @Test
    public void testSpring(){
        System.out.println(context.getBean(Dog4.class));
    }

}

15、关于自动方式注入注解

在第四章第六节有详细介绍

16、IoC的半注解方式

在第四章第五节中有介绍

三、代理模式

1、概念

​ 为了实现横向代码复用的想法,保持原有代码结构(流程)不会被改变,(不修改代码),对原有代码的功能进行增强;

​ jdk动态代理:被代理类必须要有一个实现的接口;

​ CGLIB动态代理:生成当前被代理类的子类;

​ spring对两种代理方法都支持。

2、JDK动态代理和静态代理

2.1、动态代理和静态代理概念

静态代理:在程序运行前就已经存在代理类的字节码文件,代理对象和目标对象的关系在运行前就确定了。

动态代理:动态代理类是在程序运行期间由JVM通过反射等机制动态的生成的,所以不存在代理类的字节码文件。代理对象和真实对象的关系是在程序运行事情才确定的。

2.2、静态代理实现

  • 静态代理就是再创建一个类实现service接口,将事务等操作全部在该类中完成,然后再使用该类,代用service接口的业务方法实现类。
public class EmployeeServiceImplProxy implements IEmployeeService{
    //代理对象需要有一个真实对象的引用
    @Setter
    private IEmployeeService target;
    public void save(Employee employee) {
        System.out.println("开启事务");
        try{
            //执行业务操作
            target.save(employee);
            System.out.println("提交事务");
        }catch (Exception e){
            System.out.println("回滚事务");
        }finally {
            System.out.println("关闭资源");
        }
    }
}
  • 优缺点

    • 静态代理优缺点:

      • 优点:

        ​ 1.业务类只需要关注业务逻辑本身,保证了业务类的重用性。

        ​ 2.把真实对象隐藏起来了,保护真实对象

      • 缺点:

        ​ 1.代理对象的某个接口只服务于某一种类型的对象,也就是说每一个真实对象都得创建一个代理对象。没有办法实现通用。

        ​ 2.如果需要代理的方法很多,则要为每一种方法都进行代理处理。

        ​ 3.如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

2.3、JDK动态代理实现

​ Java 动态代理机制生成的所有动态代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。

注解方式:

  • 核心代码
@Slf4j
public class PersonProxy implements InvocationHandler {
    private UserService userService;
    public  PersonProxy(UserService userService){
        this.userService = userService;
    }
    public UserService newInstance(){
        //类加载器
        ClassLoader loader=userService.getClass().getClassLoader();
        //父接口的类型,因为有可能一个类实现多个接口所以是个数组
        Class<?>[] interfaces=userService.getClass().getInterfaces();
        //因为本类实现了InvocationHandler接口,所以直接传本类对象即可
        InvocationHandler h=this;
       return  (UserService)Proxy.newProxyInstance(loader, interfaces, h);
    }
    //当findAll方法执行时,会回调该方法,所以实际增强的效果是在这里
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("findAll".equals(method.getName())) {
            log.info("这是添加的日志记录功能");
        }
        return method.invoke(userService,args);
    }
}

正常方式:

  • 核心代码
//测试类
//JDK的动态代理
    @Test
    public void testJDK() throws Exception {
        //此处通过getProxy()获取到代理对象
        IEmployeeService proxy =
                (IEmployeeService) new TransactionManagerInvocationHandler().getProxy();
        System.out.println(proxy.getClass());
        proxy.save(null);
    }
//代理类
public class TransactionManagerInvocationHandler implements InvocationHandler{
    private Object target;
    public TransactionManagerInvocationHandler(){
        ApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        IEmployeeService employeeService = context.getBean("employeeService", IEmployeeService.class);
        this.target = employeeService;
    }
    //JDK的动态代理
    public Object getProxy(){
        /*
            第一个参数为:类加载器
            第二个参数:目标对象实现的接口
                target.getClass().getInterfaces()返回字节码对象接口数组
            第三个参数:实现了InvocationHandler接口类的对象
         */
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开启事务");
        try{
            //执行业务操作(利用反射机制获取真实对象)
            method.invoke(target,args);   //调用的真实对象的该方法,
            System.out.println("提交事务...");
        }catch (Exception e){
            System.out.println("回滚事务");
        }finally {
            System.out.println("释放资源");
        }
        return null;
    }

3、CGLIB代理

​ 原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

  • CGLIB可以生成目标类的子类,并重写父类非final修饰符的方法。

  • 要求类不能是final的,要拦截的方法要是非final、非static、非private的。

  • 动态代理的最小单位是类(所有类中的方法都会被处理);

  • 核心代码

@Slf4j
public class ProductProxyWithCglib implements MethodInterceptor {


    private ProductServiceImpl productService;

    public ProductProxyWithCglib(ProductServiceImpl productService){
        this.productService = productService;
    }

    //创建代理对象
    public ProductServiceImpl createProxy(){

        Enhancer enhancer = new Enhancer();
        //设置被代理类为当前类的父类
        enhancer.setSuperclass(ProductServiceImpl.class);
        //设置回调
        enhancer.setCallback(this);

        return (ProductServiceImpl) enhancer.create();
    }
    //创建增强功能的方法
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        log.info("{}",o);

        if ("findAll".equals(method.getName())) {
            log.info("这是添加的日志记录功能");
        }

        return method.invoke(productService,objects);
    }
}

四、SpringAOP

1、AOP概述

  • ​ 在保持原有代码执行流程不被修改的情况下,对功能进行加强(代码复用)。

  • ​ 底层实现方式:

    • JDK代理
    • CGLIB代理
    • AOP概念最早由AOP联盟提出,后来Spring做了具体的实现。
  • SpringAOP术语

    • 连接点(Joinpoint)程序执行中一个精确执行点,例如一个方法调用,Spring中一般指一个方法。(所有有可能被连接到的方法)

在这里插入图片描述

  • 切入点(Pointcut)连接点的集合,这些连接点确认何时触发通知,通常采用正则、通配符语法。(真正被增强的方法)

在这里插入图片描述

  • 通知(Advice)连接点所采取的动作,如权限、日志模块(真实用于增强的代码)
    • @Before:前置通知:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
    • @AfterReturning :后置通知:在某连接点正常完成后执行的通知,通常在一个匹配的方法返回的时候执行。
    • @AfterThrowing:异常通知:在方法抛出异常退出时执行的通知。
    • @After 最终通知:当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
    • @Around:环绕通知:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

在这里插入图片描述

  • 切面(Aspect):切入点+通知结合,包含了横切逻辑的定义

在这里插入图片描述

  • Pointcut语法
    • AOP思想本应该由SUN公司来制定规范,但是被AOP联盟捷足先登了.
    • AOP联盟制定AOP规范,首先就要解决一个问题,怎么表示在哪些方法上增强—— AspectJ(语言)。
AspectJ切入点语法如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

翻译成中文:
execution(<访问修饰符>? <返回类型> <声明类型>? <方法名>(<参数>) <异常>?)

在这里插入图片描述

*:匹配任何部分,只能表示一个单词

.. :可用于全限定名中和方法参数中,分别表示子包和0到N个参数

2、模板方法设计模式

​ 在抽象类中,先设置几个抽象方法,然后设置一个非抽象方法,在非抽象方法中调用抽象方法,然后设计一个抽象类的实现类,在实现类中具体实现来几个抽象方法,最后在实例过程中,使用多态思想(父类的引用指向实现类的对象)创建一个父类对象,父类对象可以直接调用非抽象方法,操作实现类中重写的方法。(类似于回调)

3、XML方式配置AOP

  • 所需依赖
<dependencies>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    
    <!--使用AspectJ方式注解需要相应的包 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.6.11</version>
    </dependency>
    
    <!--使用AspectJ方式注解需要相应的包 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.6.11</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    
    <!--Spring基础依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    
    <!-- 整合Junit -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.6.RELEASE</version>
        <scope>test</scope>
    </dependency>
    
    <!--Spring整合事务的依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>4.3.2.RELEASE</version>
    </dependency>
    
    <!--SpringJDBC依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.7.RELEASE</version>
    </dependency>
    
    <!--SpringJDBC依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.7.RELEASE</version>
    </dependency>


    <!--德鲁伊连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>

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

    <!--mybatis整合Spring依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.0</version>
    </dependency>

    <!--mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.45</version>
    </dependency>

    <!--省略get和set方法-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>
    
    <!--日志依赖-->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    
</dependencies>
  • 被代理类
public class UserServiceImpl implements UserService {

    @Override
    public int add() {
        System.out.println("*****************add");
        return 0;
    }
}
  • 通知类
package com.offcn.asopect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;

@Slf4j
public class UserAspect {

    public Object showLog(ProceedingJoinPoint proceedingJoinPoint){
        Object proceed = null;
        try {
            log.info("日志记录");
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        return proceed;
    }
}
  • 配置文件
<?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:util="http://www.springframework.org/schema/util"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean class="com.offcn.service.impl.UserServiceImpl" id="userService"/>

    <bean class="com.offcn.asopect.UserAspect" id="userAspect"/>

    <!--****************AOP**************-->
    <aop:config>
        <!--定义全局切入点表达式-->
        <aop:pointcut id="userpointcut"  expression="execution (* com.offcn.service.impl.UserServiceImpl.*(..))"/>

        <!--配置切面-->
        <aop:aspect ref="userAspect">
            <!--配置环绕通知-->
            <aop:around  method="showLog" pointcut-ref="userpointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

4、切入点使用范围

  • 全局切入点:多个切面共享
<aop:config>
    <!--定义全局切入点表达式-->
    <aop:pointcut id="userpointcut"  expression="execution (* com.offcn.service.impl.UserServiceImpl.*(..))"/>

    <!--配置切面-->
    <aop:aspect ref="userAspect">
        <!--配置前置通知-->
        <aop:before  method="showLog" pointcut-ref="userpointcut"/>
    </aop:aspect>
</aop:config>
  • 局部切入点:一个切面共享
<aop:config>

    <!--配置切面-->
    <aop:aspect ref="userAspect">
        <!--定义局部切入点表达式-->
        <aop:pointcut id="userpointcut"  expression="execution (* com.offcn.service.impl.UserServiceImpl.*(..))"/>
        <!--配置前置通知-->
        <aop:before  method="showLog" pointcut-ref="userpointcut"/>
    </aop:aspect>
</aop:config>
  • 通知范围切入点:当前通知内使用
<aop:config>

    <!--配置切面-->
    <aop:aspect ref="userAspect">
        <!--配置前置通知-->
        <aop:before method="showLog" pointcut="execution (* com.offcn.service.impl.UserServiceImpl.*(..))"/>
    </aop:aspect>
</aop:config>
  • 关于环绕通知

    ​ 环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型,在通知体内,调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个Object[]对象-该数组中的值将被作为方法执行时的参数。

5、※批量扫描

  • 根据base-package属性值批量将java类假如到Spring容器中
<context:component-scan base-package="com.ujiuye..."/>
<!--上面这种为半注解,半xml方式代替ComponentScan-->
<mybatis:component-scan base-package=""/>
<!--上面这种为半注解,半xml方式代替MapperScan-->


<!--
对应的注解:@ComponentScan("扫描包路径路径")--此种方式为纯注解方式
但是此种方式必须要一个配置类标注两个注解:@ComponentScan("扫描包路径路径")和@Configuration
@Configuration:表示当前java类是一个spring容器配置类
然后当需要使用到注解将类交给spring管理的时候,使用注解@ContextConfiguration(classes = {配置类名.class})
-->
  • 注解分层(将类交给Spring容器管理)
@Component(""):任何类需要被管理,都可以使用该注解。

@Controller(""):标识控制器(表现层)

@Service(""):标识业务层

@Repository(""):标识持久层,注意当我们使用Mybatis框架时采用的是Mybatis本身的注解(一般不采用)。
  • 注解方式标注初始化方法和销毁方法
//初始化
@PostConstruct
public void init(){

}
//销毁
@PreDestroy
public void destroy(){

}

6、注解方式

6.1、关于自动装配有以下几种方式

纯注解方式(不需要引入额外的jar包)

为bean类中属性赋值时候,可以直接在属性上面添加@Value("")注解赋值。

依赖注入所需要的注解如下:

  • @AutoWired注解(Spring框架提供的,推荐使用)按照类型注入

    • 首先按照依赖对象的类型找,如果没有找到,默认会报错;如果找到一个匹配的对象,直接注入,如果在Spring上下文中找到多个匹配(2个或者2个以上)的类型,再按照名字去找,如果没有匹配则报错,也可以通过使用@Qualifier(“otherBean”)标签来规定依赖对象按照bean的id+类型去找;
    • @AutoWired注解可以设置reqiured=false,默认情况下为true,若设置为true则一定会去spring容器中找该对象,若没有则会报错,若设置为false,则表示当spring中存在该对象,就获取,若不存在不会报错,返回null。
    • @Qualifier(“bookDao”):使用@Qualifier指定需要装配的组件的id,而不是使用属性名(eg:@Service(“设置名称”)),相当于设置一个别名。
    • @Primary:让Spring进行自动装配的时候,默认使用首选的bean;也可以继续使用@Qualifier指定需要装配的bean的名字,被标注的bean若有相同参数,会被优先注入。
  • @Resource注解(JDK提供)按照名称注入@Service(“设置名称”)

    • 不支持@Primary功能不具备类似@Autowired(reqiured=false);的功能,但是可以通过@Resource(name="")代替,
    • 如果按照名字找不到,再按照类型去找,但如果找到多个类型匹配类型,报错,也可以直接使用name属性指定bean的名称;但是,如果指定的name,就只能按照name去找,如果找不到,就不会再按照类型去找;
  • @Inject

    • @Inject 和 @Autowired 注解一样也是按类型匹配注入的 Bean, 但没有 reqired 属性,不推荐
package com.ujiuye.spring.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;

@Configuration  // 标识当前类是Spring的一个配置类
@ComponentScan(value = {"com.ujiuye.spring"})
public class SpringConfig {
}
// 启动ioc容器(全注解模式)
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);

AccountService accountService = (AccountService) applicationContext.getBean("accountService");

Account account = accountService.saveAccount(new Account());

6.2、关于使用全注解方式扫描类交给Spring管理

  • 对应的注解:@ComponentScan(“扫描包路径路径”)–此种方式为纯注解方式
    但是此种方式必须要一个配置类标注两个注解:@ComponentScan(“扫描包路径”)和@Configuration
  • @Configuration:表示当前java类是一个spring容器配置类
  • 然后当需要使用到注解将类交给spring管理的时候,使用注解@ContextConfiguration(classes = {配置类名.class})

6.3、关于全注解注入中依赖注入(用配置类介绍)

  • 何为依赖:创建出对象之后,我们将对象中的属性全部称之为该对象的依赖;向对象中的依赖注入值称之为依赖注入。DI(依赖注入),Ioc强调创建对象的过程。

  • 若其中的方法需要交给spring管理只要在方法上面标注@Bean注解即可,相当于xml文件中的一个Bean标签。

  • 默认情况下是单例;

  • 若要设置成多例形式需要在@Bean注解下添加@Scope注解;

    @Scope("PROTOTYPE")
    //设置为多例
    
    @Scope("SINGLETON")
    //设置为单例
    
  • 关于Id:现在容器中使用的bean id值就是方法名;

  • 向注解配置类中注入值得两种方式

bean类

@Data
public class User{
	private String uname;
	//Address是另一个类,这里不给出代码
	private Address address;
}

第一种

@ComponentScan("com.ujiuye")
@Configuration
public class BeanConfig{
	@Bean
	public Address address(){
		return new Address();
	}
	@Bean
	@Scope("Singleton")
	public User user(){
		User user = new User();
		user.setUname("aaa");
		//调用上面的方法注入
		user.setAddress(address());
		return user;
	}
}

第二种

@ComponentScan("com.ujiuye")
@Configuration
public class BeanConfig{
	@Bean
	public Address address(){
		return new Address();
	}
	@Bean
	@Scope("Singleton")
	public User user(Address address){
		User user = new User();
		user.setUname("aaa");
		//通过传过来的形参注入
		user.setAddress(address());
		return user;
	}
}

7、启用注解方式AOP支持

  • 第一种

纯注解方式启用:@EnableAspectJAutoProxy,它一般和注解扫描(@ComponentScan)一起使用,但不是一定

  • 半注解半xml方式启用:
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

8、定义全局通知

@Aspect//把当前类标识为一个切面供容器读取
@Component//交给Spring管理类
@Slf4j//日志
public class UserPointCut{
    //配置全局通知
    @Pointcut("execution(* com..(..))")
    public void userPointCut(){

    }

    //引用全局通知
    @Before("userPointCut()")
    public void loginLog(){
        log.info("记录登录日志");
    }

    //引用全局通知
    @After("userPointCut()")
    public void loginLog1(){
        log.info("日志登录记录");
    }
}

在上面的引入通知注解中

9、获取AOP注解参数返回值

//javaBean
@Slf4j
@Data
public class User implements Serializable{
    private Integer uid;

    private String uname;

    private String password;

    private String nickName;
}
//注解配置类
@EnableAspectJAutoProxy
@ComponentScan("com.ujiuye")
@Configuration
public class BeanConfig {

}
//配置全局切点类
public class ServicePointCut {

    @Pointcut("execution(* com.ujiuye.service.UserServiceImpl.*(..))")
    public void userPointCut(){

    }
}



//通知类,并引用全局切入点,输出带参返回值
@Aspect
@Slf4j
@Component
public class UserPointCut {
    @Before(value = "com.ujiuye.notices.ServicePointCut.userPointCut() && args(name,pwd)")
    public void loginLog(String name,String pwd){
        log.info("获取到的参数{},{}",name,pwd);
        log.info("记录登录日志");
    }
    
    
    
    @Around(value = "com.ujiuye.notices.ServicePointCut.userPointCut() && args(user)")
    public Object add(ProceedingJoinPoint proceedingJoinPoint, User user) {
        //环绕通知
        log.info("环绕通知,参数:{}", user);
        try {
            Object proceed = proceedingJoinPoint.proceed();
            log.info("环绕通知,返回值{}", proceed);
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}


//被代理类
@Service
public class UserServiceImpl {
    public User login(String uname, String password){
        System.out.println("登录功能");
        return new User();
    }
    
    public int add(User user){
        //此处返回的是数据库受影响行数
        return 10;
    }
}
//测试类
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
@ContextConfiguration(classes = {BeanConfig.class})
public class testDemo1 {
    @Autowired
    private UserServiceImpl userService;
    @Test
    public void login(){
        //测试简单变量返回
        //userService.login("root","123456");
        //测试引用变量返回
        User user = new User();
        user.setUid(1);
        user.setNickName("hei");
        user.setUname("小黑");
        user.setPassword("123456");
        userService.add(user);
    }
}

五、Spring整合Mybatis

1、XML方式整合

1.1、依赖

  • Spring所需依赖:spring-context、spring-aspects、aspectjrt、aspectjweaver、spring-tx、spring-jdbc
  • mybatis所需要:mybatis、mybatis-spring、mysql-connector-java、(druid、c3p0、HikariCP)
  • 辅助性依赖:logback-classic、lombok、spring-test、junit、mapper(可选)、pagehelper(可选)

1.2、配置文件

  • 核心配置文件:数据库连接信息

  • 辅助性配置文件:日志配置

  • 可选配置文件:基于xml整合和半注解整合时需要的spring核心配置

1.3、整合思路

mybatis的任务

  • 构建SqlSessionFactory对象

    • 数据库的连接信息,最终以连接池的形式提供给SqlSessionFactory
  • 构建SqlSession对象

    • 通过SqlSessionFactory来获取
  • 必要的配置信息

    • 全局配置(类的别名、缓存等等)
    • 插件配置
    • 加载映射配置文件

Spring的任务

  • 管理各层的JavaBean对象

  • 管理事务

1.4、依赖列表

<!--自定义属性列表-->
    <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>

        <junit.version>4.12</junit.version>
        <spring.version>5.2.6.RELEASE</spring.version>
        <aspectjrt.version>1.6.11</aspectjrt.version>
        <aspectjweaver.version>1.6.11</aspectjweaver.version>
        <lombok.version>1.18.8</lombok.version>
        <logback.classic.version>1.2.3</logback.classic.version>
        <mybatis.version>3.4.6</mybatis.version>
        <mybatis.spring.version>1.3.2</mybatis.spring.version>
        <mysql.version>5.1.47</mysql.version>
        <HikariCP.version>3.3.1</HikariCP.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-beans</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>${HikariCP.version}</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>

        <!-- spring整合mybatis包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>${mybatis.spring.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <!--Spring基础依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!--使用AspectJ方式注解需要相应的包 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectjrt.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectjweaver.version}</version>
        </dependency>

        <!-- 整合Junit -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

        <!--省略get和set方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <!--日志依赖-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.classic.version}</version>
        </dependency>
    </dependencies>

1.5、核心代码

//方法一service实现类
public class UserServiceImpl extends SqlSessionDaoSupport implements UserService {
    private SqlSessionTemplate sqlSessionTemplate;
    public void setSqlSessionFactory(SqlSessionTemplate sqlSessionTemplate){
        this.sqlSessionTemplate = sqlSessionTemplate;
    }
    @Override
    public List<User> findAll() {
        return getSqlSession().selectList(("com.offcn.mapper.UserMapper.list"));
    }
}

//方法二service实现类
public class UserServiceImpl implements UserService {
    private UserMapper userMapper;
    public UserMapper getUserMapper() {
        return userMapper;
    }
    //UserMapper userMapper在配置文件中已经通过autowire="byType"自动注入
    //此处相当于set注入,所以有了下面这个setter方法
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    @Override
    public List<User> findAll() {
        return userMapper.list();
    }
}

<!--方法一applicationComttext.xml-->
<!--引入外部的属性文件-->
    <context:property-placeholder location="classpath:db.properties"/>
    <!--连接池:在SpringBoot2.x的版本中默认集成的连接池就是这个-->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
    </bean>
    <!--SqlSessionFactory配置-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--关联连接池-->
        <property name="dataSource" ref="dataSource" />
        <!--加载映射配置文件-->
        <property name="mapperLocations" value="classpath*:mapper/**/*.xml" />
        <!--配置别名-->
        <property name="typeAliasesPackage" value="com.offcn.domain"/>
    </bean>
    <!--构建sqlsession-->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory" />
    </bean>
    <!--将Service对应的类注册到Spring中-->
    <bean class="com.offcn.service.v1.impl.UserServiceImpl" id="userService" autowire="byType"/>



<!--方法二applicationComttext.xml-->
<!--引入外部的属性文件-->
    <context:property-placeholder location="classpath:db.properties"/>
    <!--连接池:在SpringBoot2.x的版本中默认集成的连接池就是这个-->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
    </bean>
    <!--SqlSessionFactory配置-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--关联连接池-->
        <property name="dataSource" ref="dataSource" />
        <!--加载映射配置文件-->
        <property name="mapperLocations" value="classpath*:mapper/**/*.xml" />
        <!--配置别名-->
        <property name="typeAliasesPackage" value="com.offcn.domain"/>
    </bean>
    <!--将Service对应的类注册到Spring中-->
	<!--autowire="byType将下面交给spring管理的mapper注入-->
    <bean class="com.offcn.service.v2.impl.UserServiceImpl" id="userService" autowire="byType"/>
    <!--直接使用sqlSession.getMapper()直接获取到mapper实例化对象-->
    <mybatis:scan base-package="com.offcn.mapper"/>
//方法一测试类
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
@ContextConfiguration(value = "classpath:applicationContextV1.xml")
public class Demo1 {
    @Autowired
    private UserService userServicev1;
    @Test
    public void demo1() {
        log.info("{}",userServicev1.findAll());
    }
}



//方法二测试类
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
@ContextConfiguration(value = "classpath:applicationContextV2.xml")
public class Demo2 {
    @Autowired
    private UserService userServicev2;
    @Test
    public void demo2(){
        log.info("{}",userServicev2.findAll());
    }
}

1.6、关于@ContextConfiguration()注解

  • @ContextConfiguration(value = “classpath:applicationContextV1.xml”)注解相当于,及读取配置文件

    ApplicationContext context =
            new ClassPathXmlApplicationContext("classpath:applicationContextV1.xml");
    

1.7、Spring引入配置文件标签

<!--将属性配置文件,引入到spring的配置文件-->
<context:property-placeholder location="classpath:db.properties"/>

1.8、spring的命名空间

  • spring中每次导入一个新的配置之前需要需要在头文件中引入新的命名空间

  • 在头文件中有一个叫做schemaLocation的约束空间,其规定了下面能使用哪些标签。

2、全注解方式

注意

​ 虽然被称为全注解方式,但是一些配置文件还是需要的,比如jdbc.properties,userMapper.xml,logback.xml等,这些配置文件比较简单,所以这里不给出源代码。

​ 关于@propertySource(“classpath:db.properties”)读取属性配置文件并赋值给制定的属性字段,要求必须和@Value("${}")一起使用。

​ PropertySource和Configuration注解不能一起使用,他们之间有一个加载顺序问题

2.1、核心代码

  • 两个配置类
//Spring组件注解的扫描
@ComponentScan("com.offcn")
//Mybatis接口类的扫描
@MapperScan("com.offcn.mapper")
//启用AOP注解
@EnableAspectJAutoProxy
//标注当前类是Spring的配置类
@Configuration
public class BeanConfig {
    /**
     * 构造连接池
     * @param config
     * @return
     */
    @Bean
    public DataSource dataSource(DataSourceConfig config){
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(config.getDriverClassName());
        dataSource.setJdbcUrl(config.getUrl());
        dataSource.setUsername(config.getUsername());
        dataSource.setPassword(config.getPassword());
        return dataSource;
    }
    /**
     * 构造SqlSessionFactoryBean
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        //关联连接池
        factory.setDataSource(dataSource);
        //加载配置文件
        PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = patternResolver.getResources("classpath*:mapper/**/*.xml");
        factory.setMapperLocations(resources);
        //别名
        factory.setTypeAliasesPackage("com.offcn.domain");
        return factory;
    }
}




//加载配置文件
@PropertySource("classpath:jdbc.properties")
@Component
@Data
public class DataSourceConfig {
    @Value("${jdbc.driverClassName}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
}
  • service实现类
@Service
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper mapper;
    @Override
    public List<User> list() {
        return mapper.list();
    }
    @Override
    public int insert(User user) {
        return mapper.insert(user);
    }
}
  • 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
@Slf4j
public class Demo1 {
    @Autowired
    private UserService userService;
    @Test
    public void testList(){
        log.info("{}",userService.list());
    }
    @Test
    public void testInsert(){
        User user = new User();
        user.setUsername("小黑");
        user.setPassword("456789");
        user.setAge(18);
        user.setBirthday("2020/7/27");
        userService.insert(user);
    }
}

2.2、难点

//加载配置文件
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = patternResolver.getResources("classpath*:mapper/**/*.xml");

六、事务

1、事务的ACID特性以及实现原理概述

原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。

一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。

隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。

持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。

在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面

1.事务的并发执行

2.事务故障或系统故障

数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。

并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。

在这里插入图片描述

2、spring事务简介

  • Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。主要用于处理service层事务。
  • 传播行为:当出现多个事务的时候,这些事务之间以什么样的规则进行遵守。

2.1、事务传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。

TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

2.2、事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:

TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。

TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。

TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。

TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。

TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

2.3、编程式事务

使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

public void insertDept() {
		DataSourceTransactionManager transactionManager =
 new DataSourceTransactionManager(
				jdbcTemplate.getDataSource());
		//定义一个事务
		DefaultTransactionDefinition def = new DefaultTransactionDefinition();
		//获取事务状态
		TransactionStatus status = transactionManager.getTransaction(def);
		//设置事务传播和隔离级别
		def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
		def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
		String sql = "INSERT INTO `dept`(`DEPTNO`,`DNAME`,`LOC`) VALUES(?,?,?)";
		jdbcTemplate.update(sql, 124, "三傻业务部", "火车站");
		//提交事务
		transactionManager.commit(status);
	}

每种操作都要写成上诉代码,比较麻烦。

2.4、声明式事务

  • 简介

是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

2.5、核心代码(xml方式)

<!--配置类-->
<!--将属性配置文件引入到spring的配置文件中-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--把德鲁伊连接池对象交给spring管理-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <bean id="accountDao" class="com.uijiuye.spring._02_px.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"/>
     </bean>
    <bean id="accountService" class="com.uijiuye.spring._02_px.service.impl.AccountServiceImpl">
        <property name="dao" ref="accountDao"/>
    </bean>
    <!--配置事务  配置AOP-->
    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--配置Aop-->
    <aop:config>
        <!--where-->
        <aop:pointcut id="txPointcut" expression="execution( * com.uijiuye.spring._02_px.service..*.*(..))"/>
        <!--aspect = advisor+pointcut-->
        <aop:advisor advice-ref="txAdcive" pointcut-ref="txPointcut"></aop:advisor>
    </aop:config>
    <!--配置增强器-->
    <tx:advice id="txAdcive" transaction-manager="txManager">
        <tx:attributes>
            <!--
                连接点方法(pointcut里面需要使用事务增强的方法)
                可疑使用通配符*
            -->
            <!--查询方法是只读事务,非查询方法是非只读事务-->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="select*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="list*" read-only="true"/>
            <tx:method name="*"/>
            <!--
				处理传播行为也在这里进行配置(在嵌套事务中使用),如下-->
            <tx:method name="transOut" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--
		事务的注解解析器
		此文没有注解方式处理事务,只是说明注解方式需要使用该解析器
	-->
    <tx:annotation-driven transaction-manager="txManager"/>
//dao实现类
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
    @Override
    public void transIn(Integer accountNum, Integer money) {
        String sql = "update account set balance = balance + ? where accountNum = ?";
        super.getJdbcTemplate().update(sql,money,accountNum);
    }
    @Override
    public void transOut(Integer accountNum, Integer money) {
        String sql = "update account set balance = balance - ? where accountNum = ?";
        super.getJdbcTemplate().update(sql,money,accountNum);
    }
}

2.6、核心代码(注解方式)

  • 添加注解解析器

    <tx:annotation-driven transaction-manager="txManager"/>
    
  • 在需要添加事务控制的业务方法上添加@Transactional注解

  • 默认情况下,所有的业务方法都是非只读事务,假如要设置某个方法为只读事务,那么在该方法上标注@Transactional(readOnly = true)

//设置只读
    @Transactional(readOnly = true)
    public void get(){

注意

​ 此处注解只针对事务,其余的部分任然使用xml(即使用的是半注解半xml方式)

<!--将属性配置文件,引入到spring的配置文件中-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--把德鲁伊连接池对象交给Spring管理-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <bean id="accountDao" class="com.ujiuye.spring._02_tx.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <bean id="accountService" class="com.ujiuye.spring._02_tx.service.impl.AccountServiceImpl">
        <property name="dao" ref="accountDao"></property>
    </bean>
    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--事务的注解解析器-->
    <tx:annotation-driven transaction-manager="txManager"/>
//service实现类
@Transactional
public class AccountServiceImpl implements IAccountService {
    @Setter
    private IAccountDao dao;
    @Override
    public void trans(Integer inAccountNum, Integer outAccountNum, Integer money) {
        //先从某个账户减少对应的钱
        this.transOut(outAccountNum,money);
        System.out.println( 1/0 );
        //另外一个账户加上对应的钱
        this.transIn(inAccountNum,money);
    }
    @Override
    public void transIn(Integer accountNum, Integer money) {
        dao.transIn(accountNum,money);
    }
    @Override
    public void transOut(Integer accountNum, Integer money) {
        dao.transOut(accountNum,money);
    }
    //设置只读
    @Transactional(readOnly = true)
    public void get(){
    }
}

此种方式其余部分和xml方式一致

七、SpringJDBC

1、简介

​ SpringJDBC只是Spring家族中很小的一部分,以后连接数据库还是推荐使用Mybatis,而且SpringJDBC中不存在动态SQL,一级缓存,二级缓存等操作。它的优点就是操作简单便利。

2、具体实现

//dao层实现类
public class EmployeeDaoImpl extends JdbcDaoSupport implements IEmployeeDao{

    public void save(Employee employee) {
        String sql = "insert into employee(name,age,brithday,salary) value(?,?,?,?)";
        super.getJdbcTemplate().update(sql, employee.getName(), employee.getAge(), employee.getBirthday(), employee.getSalary());

//        语法糖数组形式
//        Object[] objects = new Object[]{employee.getName(), employee.getAge(), employee.getBirthday(), employee.getSalary()};
//        this.jdbcTemplate.update(sql, objects);

    }

    public void update(Employee employee) {

        String sql = "update employee set name = ?,age=?,brithday=?,salary=? where id = ?";
        super.getJdbcTemplate().update(sql,employee.getName(),employee.getAge(),employee.getBirthday(),employee.getSalary(),employee.getId());

    }
    public void delete(Integer id) {
        String sql = "delete from employee where id =?";
        super.getJdbcTemplate().update(sql,id);
    }
    public Employee get(Integer id) {
//      queryForObject方式:返回单个员工对象
        String sql = "select * from employee where id = ?";
        Employee emp = super.getJdbcTemplate().queryForObject(
                sql,
                new Object[]{id},
                (resultSet,index)->{
                    Employee employee = new Employee();
                    employee.setId(resultSet.getInt("id"));
                    employee.setName(resultSet.getString("name"));
                    employee.setAge(resultSet.getInt("age"));
                    employee.setBirthday(resultSet.getDate("brithday"));
                    employee.setSalary(resultSet.getBigDecimal("salary"));
                    return employee;
                }//,
//                id
        );
//      三元运算符
        return emp;




//        query方式:返回集合类型员工对象
/*
        String sql = "select * from employee where id = ?";
        List<Employee> emps = super.getJdbcTemplate().query(
                sql,
                new Object[]{id},
                (resultSet,index)->{
                    Employee employee = new Employee();
                    employee.setId(resultSet.getInt("id"));
                    employee.setName(resultSet.getString("name"));
                    employee.setAge(resultSet.getInt("age"));
                    employee.setBirthday(resultSet.getDate("brithday"));
                    employee.setSalary(resultSet.getBigDecimal("salary"));
                    return employee;
                }//,
//                id
        );
//      三元运算符
        return emps.size()==1 ? emps.get(0):null;
*/
    }
    public List<Employee> list() {
        String sql = "select * from employee";
        List<Employee> emps = super.getJdbcTemplate().query(
                sql,
//                1、调用方法方式
//                new EmployeeRowMapper()
//                2、匿名内部类的方式
/*                new RowMapper<Employee>() {
                    public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
                        Employee employee = new Employee();
                        employee.setId(resultSet.getInt("id"));
                        employee.setName(resultSet.getString("name"));
                        employee.setAge(resultSet.getInt("age"));
                        employee.setBirthday(resultSet.getDate("brithday"));
                        employee.setSalary(resultSet.getBigDecimal("salary"));
                        return employee;
                    }
                }*/
//                3、lambda表达式方式
                (resultSet,index)->{
                    Employee employee = new Employee();
                    employee.setId(resultSet.getInt("id"));
                    employee.setName(resultSet.getString("name"));
                    employee.setAge(resultSet.getInt("age"));
                    employee.setBirthday(resultSet.getDate("brithday"));
                    employee.setSalary(resultSet.getBigDecimal("salary"));
                    return employee;
                }
        );
        return emps;
    }
}
//测试类
public class App {
    private ApplicationContext context;
    @Before
    public void before(){
        this.context =new ClassPathXmlApplicationContext("com/uijiuye/spring/_01_jdbc/test/App-context.xml");
    }
    @Test
    public void testSave(){
        IEmployeeDao dao = context.getBean(IEmployeeDao.class);
        Employee employee = new Employee(null,"rose",18,new Date(),new BigDecimal("100"));
        dao.save(employee);
    }
    @Test
    public void testUpdate(){
        IEmployeeDao dao = context.getBean(IEmployeeDao.class);
        Employee employee = new Employee(1,"rose",18,new Date(),new BigDecimal("200"));
        dao.update(employee);
    }
    @Test
    public void testDelete(){
        IEmployeeDao dao = context.getBean(IEmployeeDao.class);
        Integer id = 2;
        dao.delete(id)}
    @Test
    public void testGet() throws Exception {
        IEmployeeDao dao = context.getBean(IEmployeeDao.class);
        Employee employee = dao.get(3);
        System.out.println(employee);

    }
    @Test
    public void testList(){
        IEmployeeDao dao = context.getBean(IEmployeeDao.class);
        List<Employee> list = dao.list();
        for (Employee employee : list) {
            System.out.println(employee);
        }
    }
}
//xml配置文件
<!--将属性配置文件引入到spring的配置文件中-->
        <context:property-placeholder location="classpath:jdbc.properties"/>
        <!--把德鲁伊连接池对象交给spring管理-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
                <property name="driverClassName" value="${jdbc.driverClassName}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
        </bean>
        <bean id="employeeDao" class="com.uijiuye.spring._01_jdbc.dao.impl.EmployeeDaoImpl">
                <property name="dataSource" ref="dataSource"/>
        </bean>
//外部类
public class EmployeeRowMapper implements RowMapper<Employee> {
    public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
        Employee employee = new Employee();
        employee.setId(resultSet.getInt("id"));
        employee.setName(resultSet.getString("name"));
        employee.setAge(resultSet.getInt("age"));
        employee.setBirthday(resultSet.getDate("brithday"));
        employee.setSalary(resultSet.getBigDecimal("salary"));
        return employee;
    }
}

mlApplicationContext(“com/uijiuye/spring/_01_jdbc/test/App-context.xml”);
}
@Test
public void testSave(){
IEmployeeDao dao = context.getBean(IEmployeeDao.class);
Employee employee = new Employee(null,“rose”,18,new Date(),new BigDecimal(“100”));
dao.save(employee);
}
@Test
public void testUpdate(){
IEmployeeDao dao = context.getBean(IEmployeeDao.class);
Employee employee = new Employee(1,“rose”,18,new Date(),new BigDecimal(“200”));
dao.update(employee);
}
@Test
public void testDelete(){
IEmployeeDao dao = context.getBean(IEmployeeDao.class);
Integer id = 2;
dao.delete(id);
}
@Test
public void testGet() throws Exception {
IEmployeeDao dao = context.getBean(IEmployeeDao.class);
Employee employee = dao.get(3);
System.out.println(employee);

}
@Test
public void testList(){
    IEmployeeDao dao = context.getBean(IEmployeeDao.class);
    List<Employee> list = dao.list();
    for (Employee employee : list) {
        System.out.println(employee);
    }
}

}


```xml
//xml配置文件
<!--将属性配置文件引入到spring的配置文件中-->
        <context:property-placeholder location="classpath:jdbc.properties"/>
        <!--把德鲁伊连接池对象交给spring管理-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
                <property name="driverClassName" value="${jdbc.driverClassName}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
        </bean>
        <bean id="employeeDao" class="com.uijiuye.spring._01_jdbc.dao.impl.EmployeeDaoImpl">
                <property name="dataSource" ref="dataSource"/>
        </bean>
//外部类
public class EmployeeRowMapper implements RowMapper<Employee> {
    public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
        Employee employee = new Employee();
        employee.setId(resultSet.getInt("id"));
        employee.setName(resultSet.getString("name"));
        employee.setAge(resultSet.getInt("age"));
        employee.setBirthday(resultSet.getDate("brithday"));
        employee.setSalary(resultSet.getBigDecimal("salary"));
        return employee;
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值