Spring-学习笔记

Spring

官方解释是Spring为再任何平台上部署的基于Java的企业应用提供了一个综合计划和配置模板。

Spring的一个关键元素是提供了应用程序级别的基础设施搭建。Spring去关注企业应用程序的“管道”,让开发团队可以专注与自己应用的业务逻辑,而不用为具体部署环境花费更多精力。

Spring的特性

核心技术:dependency injection, events, resources, i18n, validation, data binding, type conversion, SpEL, AOP.

测试:mock objects, TestContext framework, Spring MVC Test, WebTestClient.

数据访问:transactions, DAO support, JDBC, ORM, Marshalling XML.

web框架:SpringMVC, Spring WebFlux.

集成:remoting, JMS, JCA, JMX, email, tasks, scheduling, cache.

语言:Kotlin, Groovy, dynamic languages.

Spring核心技术(下面是根据官方文档结合自己理解的翻译,方便以后阅读)

根据Spring 官方文档提供的Spring核心技术的文档来学习Spring核心技术—核心技术文档

IoC容器

Spring核心技术中最重要的就是IoC容器.

IoC容器和bean的简介

IoC — Inversion of Control(控制反转),Spring官方文档也说IoC也被理解为DI。——对象只通过构造方法参数,工厂方法参数,或者构造完成后设置的属性来决定它的依赖项,然后由容器在创建对象bean的时候来注入它的依赖项。这个过程本质上是bean的反转,由它自己通过直接构造或者使用类似服务定位器模式的机制来实例化它自己的依赖项。

IoC容器的基础包有org.springframework.beans 和 org.springframework.context。其中BeanFactory接口提供了能管理任何类型对象的高级配置机制。而ApplicationContext是BeanFactory的一个子接口它添加了如下特性:

  1. 更容易和Spring AOP集成。
  2. 消息资源处理(用于国际化)。
  3. 事件发布。
  4. 应用程序层面的特定上下文,如在Web应用中使用的WebApplicationContext。

总之,BeanFactory提供配置框架和基础功能,ApplicationContext添加更多企业级应用的特殊功能。ApplicationContext是BeanFactory的完整超集。

bean — 在Spring中构成你的应用程序主干并由IoC容器管理的对象叫做bean,也可以说bean是由IoC容器实例化,组装和管理的对象。一个bean就是你应用程序中的一个对象,Bean和Bean之间的依赖关系体现在容器使用的配置元数据中。

容器综述

在Spring中org.springframework.context.ApplicationContext接口表示Spring IoC容器,并负责实例化,配置和组装bean。容器通过读取配置元数据获得要实例化、配置和组装哪些对象的指令。配置元数据可以用XML、Java注释或Java代码三种方式。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。

ApplicationContext接口的许多实现都是由Spring提供的。在独立应用程序中,通常使用ClassPathXmlApplicationContext或FileSystemXmlApplicationContext。虽然XML是定义配置元数据的传统格式,但是您可以通过提供少量XML配置来声明启用其他附加元数据格式的支持,来指示容器使用Java注释或代码作为元数据格式。

在大多数应用程序场景中,不需要显式的用户代码来实例化Spring IoC容器的一个或多个实例。例如,在web应用程序场景中,应用程序的web. XML文件中简单的8行样板web描述符XML通常就足够了。如果使用Eclipse的Spring工具,只需单击几下鼠标或按键,就可以轻松地创建这个样板配置。

下图显示了Spring如何工作的高级视图。您的应用程序类与配置元数据相结合,这样,在创建并初始化ApplicationContext之后,您就有了一个完全配置和可执行的系统或应用程序。

在这里插入图片描述

配置元数据

如上图所示,Spring IoC容器使用一种形式(注释,Java代码,XML)来配置元数据。此配置元数据表示开发人员告诉Spring容器在应用程序中如何实例化、配置和组装对象。由于XML最为传统,所以我打算以XML为基础学习。

Spring配置必须由至少一个容器管理的(通常是多个)bean定义组成。基于xml的配置元数据将这些bean配置为顶级元素中的元素。Java配置通常在@Configuration类中使用@ bean注释的方法。

如下例子:

<?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="springTest" name="springTest" class="pojo.SpringTest"></bean>
</beans>
public class SpringTest {
    public void print(){
        System.out.println("springTest Bean");
    }
}

public class App {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("Spring.xml");
        SpringTest springTest = (SpringTest) classPathXmlApplicationContext.getBean("springTest");
        springTest.print();
    }
}

Java代码配置如下:

@Configuration
public class AppConfig {
    @Bean(name = "springTest")
    public SpringTest getSpringTest(){
        return new SpringTest();
    }
}

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        SpringTest springTest = (SpringTest) annotationConfigApplicationContext.getBean("springTest");
        springTest.print();
    }
}
Bean综述

Bean是由IoC容器和提供的配置元数据来创建的,由IoC容器来管理。

在容器内这些由配置元数据定义的bean被描述为一个叫BeanDefinition的类,其中包含如下源信息:

  1. 类名(通常是实际实现类)
  2. Bean行为配置元素,声明bean在容器中应该做哪些操作(作用域,生命周期回调等等)
  3. 依赖项
  4. 其他配置项(池大小,连接数量等)

BeanDefinition

PropertyExplained in
Class初始化Bean
Name命名bean
ScopeBean的作用范围
Constructor arguments依赖注入
Properties依赖注入
Autowiring mode自动注入模式
Lazy initialization mode懒加载模式
Initialization method初始化方法
Destruction method销毁方法

在容器需要真正实例化一个bean时,容器会通过bean的id或者name等找到对应的BeanDefinition,以它为模板来实例化一个Bean。具体创建方式可以由反射调用构造方法创建,可以由提供的静态工厂方法创建,也可以由提供的动态工厂Bean的动态工厂方法来创建。

除了容器自身创建的Bean以外,用户也可以将自己创建的对象交付给容器管理,通过ApplicationContext的BeanFactory方法获取到BeanFactory再通过Bean工厂对应的注册方法即可。

依赖注入(DI)

依赖注入是一个过程,在该过程中,对象通过构造方法的参数,工厂方法的参数或者在实例化一个对象后通过对应的setter来设置它需要的依赖项。

实际上依赖注入有两种实现方式:基于构造方法的依赖注入和基于Setter的依赖注入

基于构造方法的依赖注入

public class SpringTest {

    SubTest subTest;

    public SpringTest(SubTest subTest) {
        System.out.println("调用有参构造");
        this.subTest = subTest;
    }

    public SpringTest() {
        System.out.println("调用无参构造");
    }

    public void print(){
        subTest.print();
    }
}

/*
    配置元数据
*/
@Configuration
public class AppConfig {
    @Bean(name = "springTest")
    public SpringTest getSpringTest(){
        return new SpringTest(getSubTest());
    }

    @Bean(name="subTest")
    public SubTest getSubTest(){
        return new SubTest();
    }
}

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        System.out.println("容器初始化");
        annotationConfigApplicationContext.register(AppConfig.class);
        System.out.println("注册配置元数据");
        annotationConfigApplicationContext.refresh();
        System.out.println("容器调用refresh方法");
        System.out.println("获取bean");
        SpringTest springTest = (SpringTest) annotationConfigApplicationContext.getBean("springTest");
        springTest.print();
    }
}

执行结果:

容器初始化
注册配置元数据
调用有参构造
容器调用refresh方法
获取bean
subTest...Bean

可以看到这就是通过构造方法来实现依赖注入,通过工厂方法的实现和通过构造方法的实现相似。

基于Setter的依赖注入

基于上面例子改动一下:

public class SpringTest {

    SubTest subTest;

    public SpringTest(SubTest subTest) {
        System.out.println("调用有参构造");
        this.subTest = subTest;
    }

    public SpringTest() {
        System.out.println("调用无参构造");
    }

    @Autowired
    public void setSubTest(SubTest subTest) {
        System.out.println("调用Setter方法");
        this.subTest = subTest;
    }

    public void print(){
        subTest.print();
    }
}

@Configuration
public class AppConfig {
    @Bean(name = "springTest")
    public SpringTest getSpringTest(){
        return new SpringTest();
    }
    @Bean(name="subTest")
    public SubTest getSubTest(){
        return new SubTest();
    }
}

执行结果:

容器初始化
注册配置元数据
调用无参构造
调用Setter方法
容器调用refresh方法
获取bean
subTest...Bean

注意如果使用构造函数的注入方式,可能会出现循环依赖的情况。例如如下代码:

public class SpringTest {

    SubTest subTest;

    public SpringTest(SubTest subTest) {
        System.out.println("调用有参构造");
        this.subTest = subTest;
    }

    public SpringTest() {
        System.out.println("调用无参构造");
    }

    @Autowired
    public void setSubTest(SubTest subTest) {
        System.out.println("调用Setter方法");
        this.subTest = subTest;
    }

    public void print(){
        subTest.print();
    }
}


public class SubTest {

    private SpringTest springTest;

    public SubTest(SpringTest springTest) {
        this.springTest = springTest;
    }

    public SubTest() {
    }

    public void print(){
        System.out.println("subTest...Bean");
    }
}

@Configuration
public class AppConfig {
    @Bean(name = "springTest")
    public SpringTest getSpringTest(){
        return new SpringTest(getSubTest());
    }
    @Bean(name="subTest")
    public SubTest getSubTest(){
        return new SubTest(getSpringTest());
    }
}

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        System.out.println("容器初始化");
        annotationConfigApplicationContext.register(AppConfig.class);
        System.out.println("注册配置元数据");
        annotationConfigApplicationContext.refresh();
        System.out.println("容器调用refresh方法");
        System.out.println("获取bean");
        SpringTest springTest = (SpringTest) annotationConfigApplicationContext.getBean("springTest");
        springTest.print();
    }
}

执行后得到如下结果:

容器初始化
注册配置元数据
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.springframework.beans.factory.BeanCreationException
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:893)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
	at App.main(App.java:17)

可以看到这里抛出了beancreationException异常。

通常来说要解决循环依赖的问题我们可以使用setter注入的方式来解决循环依赖的关系。这里Spring解决循环依赖主要是通过将正在初始化但还未初始化完成的bean注入来解决的。通常情况一个 bean在作为依赖项被注入时,该bean一定是已经被完全配置的。

另外Bean的初始化也可以支持懒加载,具体配置时@Lazy或者XML中的lazy-init;但是如果一个懒加载的bean需要作为依赖项注入一个非懒加载的bean时,此时懒加载时不生效的。

自动装配

其实在实际应用中我们很少使用直接写构造方法参数的形式,因为通常情况下我们一个bean的依赖项可能有很多,这样写构造方法参数会显得非常臃肿,而且如果中途添加了新的依赖项我们需要修改构造方法的代码,所以Spring为我们提供了自动装配的功能。使用AutoWired注解或者属性可以开启自动装配。

Spring中关于自动装配的模式分为如下四种:

模式说明
no默认模式无自动装配。Bean引用必须由ref元素定义。对于大型部署,不建议更改默认设置,因为显式指定协作者可提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。
byName按属性名称自动装配。Spring寻找与需要自动装配的属性同名的bean。例如,如果一个bean定义被设置为按名称自动装配并且包含一个master属性(即它具有一个 setMaster(…)方法),那么Spring将查找一个名为master的bean定义并使用它来设置该属性。
byType如果容器中恰好存在一个该属性类型的bean,则使该属性自动装配。如果存在多个该属性类型的bean,则将引发致命异常,这表明您不能byType对该bean 使用自动装配。如果没有匹配的bean,则什么都不会发生(未设置该属性)。
constructor类似于byType但适用于构造函数参数。如果容器中不存在构造函数参数类型的一个bean,则将引发致命错误。

在使用byType或者constructor模式时,可以使用数组或者类型的集合,这样所有匹配类型的bean都可以作为候选人来满足依赖关系,自动装配的map键值时Bean的名称。

如果遇到了单个值在自动装配时匹配了容器中多个bean时有如下解决方案:

  1. 放弃自动装配。
  2. 通过将其autowire-candidate属性设置为false,来避免自动装配bean定义。
  3. 通过将单个bean定义primary的元素属性设置为true,将其指定为主要候选对象。
  4. 通过基于注释@AutoWired的配置实现更细粒度的控制。
Bean的作用域(Scope)

Spring为bean的提供了六种作用域:

scope描述
singleton默认作用域,单例模式,一个BeanDefinition对应一个实例。
prototype原型模式,一个BeanDefinition对应多个实例(懒加载)。
request请求范围,每个Http请求都有自己的bean。
session会话范围,每个Http会话都有自己的bean。
applicationServletContext范围,每个ServletContext有自己的bean。
websocket每个websocket有自己的bean。
Bean的生命周期

剩下的关于bean的还有生命周期回调方法,初始化方法,销毁方法,以及一些感知接口,还有后置处理器等等感觉拉在一起直接组成bean的生命周期更容易理解。

大概如下图 :
在这里插入图片描述

还有一些感知接口的操作我就不在途中画出了。

Spring的AOP

关于AOP是什么就不多说了。Spring提供了AspectJ注释风格的AOP功能,可以帮助我们很方便的使用AOP功能。注意这里我时风格而不等同于AspectJ在实际使用时还是Spring AOP,它不需要依赖于AspectJ编译器或织入器。

在Spring框架中IoC容器并不直接依赖与AOP框架,所以在使用IoC容器时可以不使用AOP,但是AOP为我们的IoC提供了很多强大的中间件解决方案。

在Spring中AOP主要用于:

  1. 提供声明式企业服务。此类服务中最重要的是声明式事务管理。
  2. 让用户实现自定义切面,用AOP补充他们对OOP的使用。
Spring AOP的功能和目标

Spring AOP用纯Java实现。不需要特殊的编译过程。Spring AOP不需要控制类加载器的层次结构,因此适合在servlet容器或应用服务器中使用。

关于Spring AOP的目标并不是提供完整的AOP功能,而是提供一个能与IoC容器紧密集成的AOP实现(诸如自动代理之类的功能实现)。

AOP代理

Spring AOP是一个基于代理的AOP实现,默认使用标准JDK动态代理。这允许代理任何接口(或一组接口)。

Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。在需要通知接口上没有声明的方法,或者需要将代理对象作为具体类型传递给方法的情况下,可以强制使用CGLIB。

关于JDK动态代理和CGLIB动态代理的比较:

JDK动态代理CGLIB动态代理
针对于接口针对与类
Java原生支持三方类库
@AspectJ 风格支持
public class SpringTest {

    SubTest subTest;

    public SpringTest(SubTest subTest) {
        this.subTest = subTest;
    }

    public SpringTest() {

    }

    @Autowired
    public void setSubTest(SubTest subTest) {
        this.subTest = subTest;
    }

    public void print(){
        subTest.print();
    }
}

public class SubTest {

    public SubTest() {
    }

    public void print(){
        System.out.println("subTest...Bean");
    }
}

@Component
@Aspect
public class MyAspectJ {
    @Pointcut("within(pojo.SubTest)")
    public void logPoint(){ }

    @Around("AOP.MyAspectJ.logPoint()")
    public Object logPrint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("log...before");
        Object proceed = proceedingJoinPoint.proceed();
        System.out.println("log...after");
        return proceed;
    }
}

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("AOP")
public class AppConfig {
    @Bean(name = "springTest")
    public SpringTest getSpringTest(){
        return new SpringTest(getSubTest());
    }
    @Bean(name="subTest")
    public SubTest getSubTest(){
        return new SubTest();
    }
}

public class App {
    public static void main(String[] args) throws InterruptedException {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        annotationConfigApplicationContext.register(AppConfig.class);
        annotationConfigApplicationContext.refresh();
        SpringTest springTest = (SpringTest) annotationConfigApplicationContext.getBean("springTest");
        springTest.print();
    }
}

效果如下:

log...before
subTest...Bean
log...after

Spring其他核心技术

除了DI和AOP以外Spring的核心技术还包含,事件,数据绑定,SpEL,i18n,类型转换,验证器等等。这里就不一一赘述了。

Spring数据访问 (官方文档)

数据访问以及数据访问层与业务或服务层之间的交互的功能。

事务管理

全面的事务支持是使Spring引人注目的原因之一。Spring框架为事务管理提供了一致的抽象,它提供了以下好处:

  • 跨不同事务API(如Java事务API (JTA)、JDBC、Hibernate和Java持久性API (JPA))的一致编程模型。
  • 支持声明式事务管理。
  • 用于程序化事务管理的API比复杂事务API(如JTA)更简单。
  • 优秀的与Spring的数据访问抽象集成。
Spring框架的事务支持模型的优点

传统上,Java EE开发人员有两种事务管理选择:全局事务或本地事务,这两种选择都有很大的局限性。接下来的两部分将回顾全局和本地事务管理,然后讨论Spring框架的事务管理支持如何解决全局和本地事务模型的局限性。

全局事务

全局事务允许使用多个事务资源(例如:关系型数据库,或者消息队列),应用服务器通过JTA事务来管理全局事务,JTA事务的API相对繁琐并且JTA的UserTransaction通常来自JNDI,这意味着还需要使用JNDI才能使用JTA。(现在除了非常大的老系统应该没人用JNDI了吧)。

以前,使用全局事务一般选择CMT。CMT是声明式事务的一种形式,它为程序员省去了查找事务相关的JNDI的步骤,但是EJB本身需要使用JNDI。它消除了大部分通过JAVA代码来控制事务的步骤,但是它是建立在JTA和应用服务器环境上的,而且必须在你的业务中使用EJB你才能使用CMT。但是EJB的缺点(复杂的API等)让CMT在和其他声明式事务相比时显得没有竞争力。

本地事务

本地事务拥有特定的资源,比如一个事务对应一个JDBC连接。相对于全局事务来说本地事务更容易理解和使用,但是它不能完成跨资源(比如多个数据库)的事务工作。还有就是本地事务的使用需要在代码中体现比如commit,rollback。

Spring提供的一致编程模型

在Spring中解决了本地事务和全局事务的缺点。它允许开发人员在任何环境中使用一致的编程模型,开发人员可以使用同样的代码在不同的环境不同的事务管理方式下运行。Spring提供了声明式事务和编程式事务两种方式。

通过编程事务管理,开发人员使用Spring抽象事务模型,它可以运行在任何底层事务基础设施上。如果使用声明式事务,开发人员通常不用或者很少写事务相关的代码,所以也不依赖于Spring事务的API或者其他任何事务的API。

理解Spring抽象事务模型

Spring事务抽象的关键是事务策略的概念。Spring中事务策略通过TransactionManager来定义,其中像org.springframework.transaction.PlatformTransactionManager接口定义了传统式事务管理,org.springframework.transaction.ReactiveTransactionManager定义了响应式事务管理。

先说说传统式事务管理:

PlatformTransactionManager:

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

它主要是一个服务提供者接口(SPI),但是也可以在应用程序代码中编程式的使用它。因为它是一个接口所以可以根据具体需要来具体实现。它也不与像JNDI之类的任何resource绑定。也就是说PlatformTransactionManager的实现类可以是IOC容器中的任何Bean。仅仅这种效果就会让开发人员十分满意,就算你在使用JTA,这种抽象也会让你测试事务代码时更轻松。

并且为了保证Spring API简洁的理念,接口中抛出的TransactionException异常都定义为了运行时异常。通常来说事务基础操作失败时十分致命的。但是在极少数可以恢复事务的情况下,开发者同样可以通过捕获TrasactionException来恢复事务。

getTransaction(…)方法根据TransactionDefinition参数返回一个TransactionStatus对象。如果当前调用堆栈中存在匹配的事务,则返回的TransactionStatus可能是一个新事务,也能是一个现有事务。后一种情况的含义是Java EE事务上下文一样。

关于响应式事务管理,Spring 5.2为支持响应式事务提供了一个接口:

ReactiveTransactionManager

public interface ReactiveTransactionManager extends TransactionManager {

    Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;

    Mono<Void> commit(ReactiveTransaction status) throws TransactionException;

    Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

它具有和PlatformTransactionManager类似的抽象含义。

可以看到上面getTransaction方法中都涉及一个接口TransactionDefinition,它主要是定义事务的一些信息:

  • Propagation:传播性
  • Isolation:隔离性
  • Timeout:超时时间
  • Read-only status:标注只读状态

上面还提到一个TransactionStatus的接口,这个接口主要用于控制事务或者查询事务状态:

TransactionStatus

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    //是否是新事务
    @Override
    boolean isNewTransaction();
    //是否存在存储点
    boolean hasSavepoint();
    //设置事务回滚
    @Override
    void setRollbackOnly();
    //事务是否回滚
    @Override
    boolean isRollbackOnly();
    //刷新事务
    void flush();
    //事务是否完成
    @Override
    boolean isCompleted();
}

无论您在Spring中选择声明式事务管理还是编程式事务管理,都必须定义TransactionManager的实现类。(通常通过依赖注入来定义)

TransactionManager的实现类通常需要知道自身的运行环境是什么(比如:JDBC,JTA,Hibernate等等),下面的示例展示了如何定义本地PlatformTransactionManager实现:

<!--1.定义数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <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>
<!--1.定义TransactionManager-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果使用JTA:

<?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:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        https://www.springframework.org/schema/jee/spring-jee.xsd">

    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>

    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />

    <!-- other <bean/> definitions here -->

</beans>

如果使用hibernate:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

注意不管你使用什么数据访问技术,只要你使用了JTA那么TrasactionManager就因该是JtaTransactionManager。因为JTA事务时全局事务可以使用任何事务资源。

由于TransactionManager抽象的缘故,不管你使用那种情况应用程序的代码都不需要修改,只需要修改配置即可。

事务的使用

上面讲了如何建立TransactionManager和讲事务管理器连接到对应的数据源。这里讲下代码中如何使用事务管理器。

高级同步方法

首选的方法是使用Spring的最高级的基于模板的持久性集成api,或者使用本地ORM api和事务感知的工厂bean或代理来管理本地资源工厂。这些可感知事务的解决方案在内部处理资源创建和重用、清理、资源的可选事务同步和异常映射。因此,用户数据访问代码不必处理这些任务,可以只关注非样板的持久性逻辑。通常,使用本机ORM API或通过使用JdbcTemplate采用模板方法进行JDBC访问

低级同步方法

像DataSourceUtils(用于JDBC)、EntityManagerFactoryUtils(用于JPA)、SessionFactoryUtils(用于Hibernate)等类存在于较低的级别上。

例如,在JDBC的情况下,您可以使用Spring的org.springframework.jdbc.datasource代替调用数据源上的getConnection()方法的传统JDBC方法。DataSourceUtils类如下:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果现有事务已经有一个与之同步(链接)的连接,则返回该实例。否则,方法调用将触发一个新连接的创建,该连接将(可选地)同步到任何现有事务,并可用于随后在同一事务中重用。如前所述,任何SQLException异常都被包装在Spring框架CannotGetJdbcConnectionException中,后者是Spring框架中未检查的DataAccessException类型的层次结构之一。这种方法为您提供了比从SQLException更容易获得的信息,并确保跨数据库、甚至跨不同持久性技术的可移植性。

这种方法在没有Spring事务管理的情况下也可以工作(事务同步是可选的),因此无论是否使用Spring进行事务管理,您都可以使用它。

当然,一旦您使用了Spring的JDBC支持、JPA支持或Hibernate支持,您通常不喜欢使用DataSourceUtils或其他辅助类,因为与直接使用相关api相比,您更喜欢使用Spring抽象。例如,如果您使用Spring JdbcTemplate或jdbc.object来简化JDBC的使用,正确连接检索是在后台进行的,不需要编写任何特殊代码。

TransactionAwareDataSourceProxy

在非常底层存在一个叫TransactionAwareDataSourceProxy的类,该类是为了代理数据源,将数据源交给Spring事务管理。在这方面,它类似于由Java EE服务器提供的事务性JNDI数据源。一般来说没用,除非你希望直接使用数据源。

声明式事务

声明式事务的代码侵入性非常小。Spring的声明式事务管理是通过Spring AOP实现的。当然如果你不理解AOP概念也不妨碍你的使用。

Spring框架的声明式事务管理类似于EJB CMT,因为您可以将事务行为(或不指定)指定到单个方法级别。如果需要,可以在事务上下文中进行setRollbackOnly()调用。这两种事务管理类型的区别是:

  • 与绑定到JTA的EJB CMT不同,Spring框架的声明式事务管理可以在任何环境中工作。
  • 您可以将Spring声明式事务管理应用到任何类,而不仅仅是ejb等特殊类。
  • Spring框架提供了声明式回滚规则,这一特性在EJB中没有对应的功能。
  • Spring框架允许使用AOP自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意通知以及事务性通知。使用EJB CMT,您不能影响容器的事务管理,除非使用setRollbackOnly()。
  • Spring框架不像高端应用服务器那样支持跨远程调用传播事务上下文。如果需要使用这个功能建议使用EJB。但是,在使用此类功能之前要仔细考虑,因为通常不希望事务跨越远程调用。

回滚规则的概念很重要。它们允许您指定哪些异常(或者Throwables)应该导致自动回滚。在Spring中你可以在配置中声明性地指定,而不是在Java代码中。因此,尽管您仍然可以在TransactionStatus对象上调用setRollbackOnly()来回滚当前事务,但大多数情况下,您可以指定一条规则,即MyApplicationException必须始终导致回滚。此选项的显著优点是业务对象不依赖于事务基础结构。例如,他们通常不需要导入Spring事务api或其他Spring api。

虽然EJB容器默认行为会自动回滚系统异常(通常是运行时异常)上的事务,但是EJB CMT不会自动回滚应用程序异常(即,除了java. rm. remoteexception之外的检查异常)上的事务。虽然声明性事务管理的Spring默认遵循EJB约定(仅在未检查的异常上自动回滚),但是也经常需要自定义回滚规则。

理解Spring声明式事务的实现

上面也提到了Spring中声明式事务的支持是通过AOP实现的,而事务的增强是通过元数据(如xml,注解)来驱动的。Spring会根据你的元数据为对应的事务管理器生成代理类。这个代理类通过使用TransactionInterceptor与对应的TransactionManager实现来把方法添加到事务中。

TransactionInterceptor中提供了对于传统事务和响应式事务的编程模型。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应类型(如Publisher或Kotlin Flow(或它们的子类型))的方法符合响应事务管理。其他所有类型都使用传统事务管理。

上面也提过了事务管理风格影响所需的事务管理器。传统式事务需要一个PlatformTransactionManager,而响应式事务使用ReactiveTransactionManager实现。

下图显示了在事务代理上调用方法的概念视图:

在这里插入图片描述

声明式事务实现的示例

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}

public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}

假设FooService接口的前两个方法getFoo(字符串)和getFoo(字符串,字符串)必须在具有只读语义的事务上下文中运行,而其他方法insertFoo(Foo)和updateFoo(Foo)必须在具有读写语义的事务上下文中运行。下面几段将详细解释下面的配置:

<!-- from the file 'context.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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!--事务增强(如果事务管理器bean的名称为TransactionManager 可以省略)-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- 所有方法名与get*匹配的方法都开启只读事务 -->
            <tx:method name="get*" read-only="true"/>
            <!-- 其他所有方法使用传统事务 -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- aop配置 -->
    <aop:config>
        <!-- 配置切点为所有FooService接口下的所有方法 -->
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <!-- 配置增强为刚刚的事务增强 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- 数据源配置 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <!-- TransactionManager配置 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

当你需要整个Service层都是事务性的时候,你可以通过设置aop配置来实现:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

如果要使用响应式事务管理需要响应式类型:


public interface FooService {

    Flux<Foo> getFoo(String fooName);

    Publisher<Foo> getFoo(String fooName, String barName);

    Mono<Void> insertFoo(Foo foo);

    Mono<Void> updateFoo(Foo foo);

}

public class DefaultFooService implements FooService {

    @Override
    public Flux<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Publisher<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}
声明式事务的回滚

要向Spring框架的事务基础结构表明事务的工作要回滚,推荐的方法是从当前在事务上下文中执行的代码中抛出异常。

在默认配置中,Spring只会在运行时异常抛出的情况下回滚。默认情况下Error实例也会造成回滚。抛出检查性异常在默认配置下不会造成事务回滚。

前面也说了你可以自定义异常回滚的条件,像如下示例:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

当然也可以让事务回滚忽视某些异常:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

当你及配置了rollback-for也配置了no-rollback-for时,最有限制力的匹配规则胜出。像下面,InstrumentNotFoundException也不会抛出异常:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

当然也可以使用编程的方式来实现自定义代码回滚:

public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
为不同的bean配置不同的事务语义

比如:你在Service层有很多的Service,你希望对不同的Service设置不同的事务时可以像下面这样:

当有的Bean需要事务而有的不需要时:

<?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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="serviceOperation"
                expression="execution(* x.y.service..*Service.*(..))"/>

        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>

    </aop:config>

    <!-- 者两个Bean需要事务 -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... 这两个不需要 -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- 设置在不同包中 -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- 或者不以Service结尾 -->

    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

</beans>

当两个Bean需要不同的事务时:

<?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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:config>

        <aop:pointcut id="defaultServiceOperation"
                expression="execution(* x.y.service.*Service.*(..))"/>

        <aop:pointcut id="noTxServiceOperation"
                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>

        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>

        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>

    </aop:config>

    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>

    <tx:advice id="defaultTxAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <tx:advice id="noTxAdvice">
        <tx:attributes>
            <tx:method name="*" propagation="NEVER"/>
        </tx:attributes>
    </tx:advice>

</beans>
<tx:advice/>

<tx:advice>可以设置如下属性:

  1. 传播性
  2. 隔离性
  3. 读写性
  4. 超时事件
  5. 回滚规则

<tx:attributes>中嵌套的<tx:method>可以设置如下属性:

属性名是否必须默认值说明
name要与事务属性关联的方法名可以用通配符*
propagationREQUIRED事务传播特性
isolationDEFAULT事务隔离级别。仅适用于REQUIRED或REQUIRES_NEW的传播设置。
timeout-1事务超时时间(秒)。仅适用于传播特性为REQUIRED或REQUIRES_NEW。
read-onlyfalse是否只读。只适用于REQUIRED或REQUIRES_NEW。
rollback-for触发回滚的异常类型,逗号分割多个。
no-rollback-for不触发回滚的异常类型,逗号分割多个。
@Transactional 的使用

例子:

@Transactional
public class DefaultFooService implements FooService {

    Foo getFoo(String fooName) {
        // ...
    }

    Foo getFoo(String fooName, String barName) {
        // ...
    }

    void insertFoo(Foo foo) {
        // ...
    }

    void updateFoo(Foo foo) {
        // ...
    }
}

直接注解在类上这样表示该类的所有方法都需要事务管理,注意@Transactional只会在被注解的具体类上生效,而不在其子类上生效。当然也可以注解具体的方法。如果非要在接口上使用注解,则该类在Spring中的运作必须是基于接口的代理事务才能生效。

@Transactional的可设置值:

属性类型描述
valueString可选限定符,指定要使用的事务管理器。
propagationenum: Propagation设置传播特性
isolationenum: Isolation可选的隔离级别。仅适用于REQUIRED或REQUIRES_NEW的传播值。
timeoutint(以秒为单位)可选的事务超时时间。仅适用于REQUIRED或REQUIRES_NEW的传播值。
readOnlyboolean读写事务与只读事务。只适用于REQUIRED或REQUIRES_NEW的值。
rollbackForClass[]必须导致回滚的可选异常类数组。
rollbackForClassNameString[]必须导致回滚的异常类的可选名称数组。
noRollbackForClass[]不导致回滚的可选异常类数组。
noRollbackForClassNameString[]不导致回滚的异常类的可选名称数组。

事务的传播特性

PROPAGATION_REQUIRED

还是用一个图来解释:

在这里插入图片描述

PROPAGATION_REQUIRED执行在一个事务下,如果没有事务存在,可以在本地处理当前作用域,也可以参与到更大作用域定义的现有“外部”事务。例如,一个Service需要多个repository ,其中所有repository的资源都必须参与Service级事务。

PROPAGATION_REQUIRED 状态下,Spring会为每个事务性方法创建一个逻辑事务,每个逻辑事务都可以单独设置回滚状态,外部事务在逻辑上独立于内部事务。在标准情况下,所有的逻辑事务都映射在一个相同的物理事务上所以内部事务的回滚确实会影响外部事务的提交。但是因为处于一个物理事务,那么内部事务的隔离级别,超时时间,读写特性都是继承自外部事务的。当然也可以通过validateExistingTransactions来设置是否验证这些属性。

在内部事务设置回滚时,外部事务并没有根据自身决定是否回滚,所以这个回滚是预料之外的,对应的一个UnexpectedRollbackException会在此时抛出,通过这个异常让外部事务感知内部事务的回滚。而外部调用者也可以通过该异常知道事务发生了回滚。

PROPAGATION_REQUIRES_NEW

在这里插入图片描述

PROPAGATION_REQUIRES_NEW和PROPAGATION_REQUIRED相比他总是使用独立物理事务,内部事务的提交和回滚不会影响外部事务,当然也具有自己的隔离级别,超时时间,和读写特性。

PROPAGATION_NESTED

PROPAGATION_NESTED使用一个具有多个保存点的物理事务,该事务可以回滚到多个保存点。这样的部分回滚允许内部事务作用域触发其作用域的回滚,使得外部事务能够继续物理事务。这个设置通常映射到JDBC保存点上,因此它只对JDBC资源事务有效。

其他
  1. PROPAGATION_REQUIRED(默认实现):当前没有事务则新建事务,有则加入当前事务

  2. PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务则以非事务方式执行

  3. PROPAGATION_MANDATORY:使用当前事务,如果没有则抛出异常

  4. PROPAGATION__REQUIRES_NEW:新建事务,如果当前有事务则把当前事务挂起

  5. PROPAGATION_NOT_SUPPORIED:以非事务的方式执行,如果当前有事务则把当前事务挂起

  6. PROPAGATION_NEVER:以非事务的方式执行,如果当前有事务则抛出异常

  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行。

Transaction-bound 事件

在Spring 4.2中,一个事件的侦听器可以绑定到事务的一个阶段。典型的例子是在事务成功完成时处理事件。

您可以使用@EventListener注释注册一个常规事件监听器。如果需要将其绑定到事务,请使用@TransactionalEventListener。当您这样做时,默认情况下侦听器被绑定到事务的提交阶段。

下一个示例展示了这个概念。假设一个组件发布了一个订单创建的事件,并且我们希望定义一个侦听器,该侦听器应该只在发布该事件的事务成功提交后处理该事件。下面的示例设置了这样一个事件侦听器:

@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        // ...
    }
}

@TransactionalEventListener有一个phase属性,该属性允许您自定义侦听器应该绑定到的事务其他阶段。有效的值是BEFORE_COMMIT、AFTER_COMMIT(默认)、AFTER_ROLLBACK和AFTER_COMPLETION。

DAO支持

Spring中的DAO支持技术旨在以一致的方式简化数据访问技术(如JDBC、Hibernate或JPA)的使用。这使您可以相当容易地在上述持久性技术之间切换,而且还使您在编写代码时不必担心捕获特定于每种技术的异常。

一致的异常层次结构

Spring提供了从特定于技术的异常(比如SQLException)到它自己的异常类层次结构其中DataAccessException作为根异常的方便转换。这些异常封装了原始异常,这样您就不会有丢失任何可能出错的信息的风险。

除了JDBC异常之外,Spring还可以封装JPA和hibernate特有的异常,将它们转换为一组集中的运行时异常。这允许您仅在适当的层中处理大多数不可恢复的持久性异常,而不必在dao中使用烦人的样板捕获和抛出块以及异常声明。如上所述,JDBC异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着您可以在一致的编程模型中使用JDBC执行一些操作。

前面的讨论适用于Spring对各种ORM框架的支持中的各种模板类。如果您使用基于拦截器的类,应用程序必须关心处理hibernateexception和persistenceexception本身,最好是通过分别委托给SessionFactoryUtils的convertHibernateAccessException(…)或convertJpaAccessException()方法。这些方法将异常转化为能和org.springframework.dao 层面异常兼容的异常。由于PersistenceExceptions是未检查异常所以他们也可能被抛出。

下图显示了Spring提供的异常层次结构(不完整):

在这里插入图片描述

用于配置DAO或Repository类的注释

确保数据访问对象(dao)或存储库提供异常转换的最佳方法是使用@Repository注释。这个注释还允许组件扫描支持查找和配置dao和Repository,而不必为它们提供XML配置条目。例如:

@Repository 
public class SomeMovieFinder implements MovieFinder {
    // ...
}

任何DAO或Repository实现都需要访问持久性资源,这取决于所使用的持久性技术。例如,基于JDBC的存储库需要访问JDBC数据源,而基于jpa的存储库需要访问EntityManager。

实现这一点最简单的方法是通过使用@Autowired, @Inject, @Resource或@PersistenceContext注解来注入这个资源依赖。下面的例子适用于JPA Repository:

@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}

如果你使用经典的Hibernate api,你可以注入SessionFactory,如下面的例子所示:

@Repository
public class HibernateMovieFinder implements MovieFinder {

    private SessionFactory sessionFactory;

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    // ...
}

我们在这里展示的最后一个示例是典型的JDBC支持。您可以将数据源注入到初始化方法或构造函数中,通过使用该数据源,您可以在其中创建JdbcTemplate和其他数据访问支持类(例如SimpleJdbcCall和其他类)。下面的例子自动生成一个数据源:

@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void init(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // ...
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值