深入剖析实战Spring

引言

此篇文章结合各路大神文章以及自己的一些拙见,为更好的学习和之后的深入了解而作,不足之处还望提出。

开始学习前建议自己创建一个简单的spring项目。
Eclipse搭建一个最简单的Spring框架Maven项目附带XML显式配置装载demo

JavaEE体系结构包括四层,从上到下分别是应用层、Web层、业务层、持久层。Struts和SpringMVC是Web层的框架,Spring是业务层的框架,Hibernate和MyBatis是持久层的框架。

Spring

概念

Spring是一个开源的容器框架,使用基本的JavaBean代替EJB。它主要是为了解决企业应用开发的复杂性而诞生的。简单来说,Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架。

Spring的优点

  • AOP和IOC。
    下面做详细介绍
  • 低侵入式设计,代码污染度很低。
    基于Spring开发的应用一般不依赖于spring的类。
  • DI机制将对象之间的依赖关系交给Spring控制管理,方便解耦。
    高内聚低耦合
  • 声明式事务的支持。
    只需要通过配置就可以完成对事务的管理,而无需手动编程
  • 支持集成各种优秀的框架。
    如Hibernate,Struts2,Mybatis等。

Spring的缺点

  • 使用XML配置复杂繁琐容易出错。
  • 粘性大,框架集成后不容易拆分。

组成Spring框架的七大模块

  • 结构图。
    在这里插入图片描述
  • 核心容器(Spring Core)
  1. 提供 Spring框架的基本功能;
  2. 主要组件是 BeanFactory(Spring的核心类),负责产生和管理Bean,它是工厂模式的经典实现;
  3. 提供控制反转 (IOC)和依赖注入(DI)特性,将应用的配置和依赖性规范与实际的应用程序代码分开。
  • Spring 上下文(Spring Context)
  1. 是一个配置文件,向 Spring框架提供上下文信息。
  2. SpringContext模块继承BeanFactory类,添加了事件处理、国际化、资源装载、透明装载、以及数据校验等功能;
  3. 还提供了框架式的Bean的访问方式和企业级的功能,如JNDI访问,支持EJB、远程调用、继承模板框架、Email和定时任务调度等。
  • 面向切面编程(Spring AOP)
  1. 直接将面向方面的编程功能集成到了Spring框架中,使 Spring框架管理的任何对象支持 AOP;
  2. 为基于 Spring 的应用程序中的对象提供了事务管理服务;
  3. 通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • JDBC和DAO模块(Spring DAO)
  1. DAO(DataAccessObject)模式思想是将业务逻辑代码与数据库交互代码分离,降低两者耦合;
  2. 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息;
  3. 异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。
  • 对象关系映射(Spring ORM)
  1. Spring框架插入了若干个 ORM 框架;
  2. 提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map,所有的这些都遵从Spring的通用事务和DAO异常层次结构;
  3. 注意这里Spring是提供各类的接口(support),目前比较流行的下层数据库封闭映射框架,如ibatis,Hibernate等。
  • Spring Web
  1. 此模块建立在SpringContext基础之上,提供了Servlet监听器的Context和Web应用的上下文;
  2. 对现有的Web框架,如JSF、Tapestry、Structs等提供了集成;
  3. SpringWeb模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring Web MVC
  1. 建立在Spring核心功能之上,拥有Spring框架的所有特性,能够适应多种多视图、模板技术、国际化和验证服务,实现控制逻辑和业务逻辑的清晰分离;
  2. 通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI;
  3. MVC模型待完善(将会新写一篇关于Spring Mvc框架的总结)。

IOC(控制反转)/ DI(依赖注入)

IoC和DI有什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

定义理解何为IOC

IOC主要是为了解决对象与对象之间的复杂的依赖关系,借助于“第三方”实现具有依赖关系的对象之间的解耦。IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

获得依赖对象的过程由自身管理变为了由IOC容器主动注入。
在未引入IOC之前对象之间的依赖关系通过自身的相关调用,这是一个主动行为;在引入IOC之后容器将对象之间的依赖关系解耦,此时对象之间失去了直接的联系,获取依赖的过程从主动行为被成了被动行为,控制权颠倒。

对象A依赖于对象B,当对象 A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象A。IOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
图1:软件系统中耦合的对象在这里插入图片描述
架构师之路(39)—IoC框架
Inversion of Control Containers and the Dependency Injection pattern
深度理解依赖注入(Dependence Injection)
Inside ObjectBuilder Part1

DI注入的几种方式

创建应用对象之间协作关系的行为通常称为装配( wiring ),这也是依赖注入( DI )的本质。

  1. 在 XML 中进行显式配置。
  2. 在 Java 中进行显式配置
  3. 隐式的 bean 发现机制和自动装配

在 XML 中进行显式配置

1、spring依靠xml文件进行显式装配,xml文件的根节点是,包含多个节点
2、xml文件中每个bean节点可以指定对应组件的类型,名称
3、xml文件中,通过bean的property子节点实现属性初始化或依赖注入
4、xml文件中,通过bean的constructor-arg子节点实现构造器参数初始化或依赖注入
5、xml文件中,通过property或constructor-arg的子节点或实现列表的注入
6、所有通过xml显式配置的bean,在代码中通过上一篇文章所描述的应用上下文实现ClassPathXmlApplicationContext从容器中获取实体
《Spring实战》-第二章:Bean的装配(1)-XML显式装配
PS:constructor-arg注入时使用name value属性可能会报错

Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 8 in XML document from class path resource [gz.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 8; columnNumber: 49; cvc-complex-type.3.2.2: 元素 'constructor-arg' 中不允许出现属性 'vaule'。
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:399)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:181)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:217)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:252)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:127)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:93)
	at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
	at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:613)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:514)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
	at gztest.Test.main(Test.java:8)
Caused by: org.xml.sax.SAXParseException; lineNumber: 8; columnNumber: 49; cvc-complex-type.3.2.2: 元素 'constructor-arg' 中不允许出现属性 'vaule'。

解决办法:
1、更改spring版本,下面代码最后一行的最右边改成spring-beans-4.0.xsd。

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 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

2、使用index方式注入

<bean id="person" class="gztest.Person">
	<constructor-arg index="0">
	<value>daniel</value>
	</constructor-arg>
	<constructor-arg index="1">
	<value>18</value>
	</constructor-arg>
</bean>

在 Java 中进行显式配置

1、JavaConfig显式配置相对于XML显式配置来说,具有配置简单,维护方便的优点
2、JavaConfig显式配置主要依赖于@Bean注解和@Configuration两个注解实现,其中@Configuration注解标明当前类为配置类,而@Bean使用在配置类中的方法内,定义一个装配类名为方法名的Bean
3、Spring使用@Autowired进行依赖注入
4、Spring中我们可以通过两种方式方便的对javaConfig显式配置进行测试,一种是结合SpringJUnit4ClassRunner模拟装配环境进行测试,一种是使用应用上下文AnnotationConfigApplicationContext进行测试
《Spring实战》-第二章:Bean的装配(2)-JavaConfig显式装配
模拟装配环境示例

@RunWith(SpringJUnit4ClassRunner.class)//使用Spring提供的测试包进行测试,主要帮助实现bean的装载环境
@ContextConfiguration(loader = AnnotationConfigContextLoader.class,classes = {CDConfig.class})//配置类指向CDConfig
public class AppTest 
{
    //使用注解自动注入
    @Autowired
    private CDPlayer cdPlayer;
 
    /**
     * Rigorous Test :-)
     */
    @Test
    public void play()
    {
        this.cdPlayer.playCD();
    }
}

隐式的 bean 发现机制和自动装配

1、自动化装配使用全注解方式替代XML和JavaConfig显式装配方式,方便简洁
2、自动化装配依赖于几个特殊注解:@Autowired,@Component,@ComponentScan和@Configuration
3、@Component注解可以将类定义为装配的组件,同时可以为改组件另起别名(ID)
4、@Configuration将使用该注解的当前类标注为配置类,@ComponentScan开启自动扫描,默认扫描当前配置类所在的包及其子包中含有或使用了@Component注解的类,可通过属性指定扫描范围
5、@Autowired注解可以用在属性,构造函数,setter以及任何一个普通方法中,是Spring依赖注入的核心注解
《Spring实战》-第二章:Bean的装配(3)-自动化装配
指定扫描包的几种方式

//@ComponentScan(basePackages = {"com.my.spring","com.my.test"})//指定扫描com.my.spring包和com.my.test包及其子包下的组件
//@ComponentScan(basePackageClasses = {App.class})//指定扫描App.class所在包及其子包下的组件
@ComponentScan//仅能扫描自身所在包及其子包的组件
@Configuration
public class CDConfig {}
//注:通常,为了防止因包路径更改以及业务实体类更改等因耦合而产生的问题,我们通常会使用一个不具备任何业务意义的空接口作为扫描包的类

混合导入装配机制

最常用的方式,将多个javaConfig或多个XML配置统一到一个文件中进行管理的技术,这样有利于进行配置管理(UCM,既统一配置文件管理,也就是所有配置文件统一加载、统一读取。)
《Spring实战》-第二章:Bean的装配(4)-混合导入装配机制

AOP(面向切面编程)

概念

在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

程序在执行具体业务逻辑前、后,可能需要加入一些额外的东西,譬如:日志,事务,安全等。在很多地方都需要加入这些额外的元素,但是这些方面和具体的业务逻辑并没有直接的关系。于是乎,就将这些额外的元素抽出来,加以封装成一个特殊的切面类(aspect),在特定的位置横切到方法的业务逻辑前后等位置。将业务和非业务的分开,极大程度解耦。

举个例子,当你的程序写好后,需要在所有的业务操作中添加一条日志,传统的做法是直接改造每个方法,但是这样势必让代码变糟糕,要是以后扩展起来那就更乱了。而AOP的思想就让你能从一个切面的角度来看待这些插入问题,AOP允许你以一种统一的方式在运行时期在想要的地方插入这些逻辑。

复杂性模型

如果将日志,事务,安全这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。
实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。
即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。
组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务
在这里插入图片描述
上图展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还要亲自执行这些服务。

AOP的作用

在整个系统内,关注点(例如日志、安全和事务)的调用经常散布到各个模块中,而这些关注点并不是模块的核心业务。

AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务所带来复杂性。总之,AOP能够确保POJO的简单性。

如下图所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在。这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。
在这里插入图片描述
利用AOP, 系统范围内的关注点覆盖在它们所影响组件之上

AOP术语

①通知( Advice ):切面的工作被称为通知。通知描述切面的工作,同时决定切面何时工作【定义了切面工作做什么,什么时候做】

  • 前置通知( Before ):在目标方法被调用之前调用通知功能;
  • 后置通知( After ):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  • 返回通知( After-returning ):在目标方法成功执行之后调用通知;
  • 异常通知( After-throwing ):在目标方法抛出异常后调用通知;
  • 环绕通知( Around ):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

②连接点( Join point ):触发切面工作的点,比如方法执行,异常抛出等行为

③切点( Poincut ):决定切面工作的地方,比如某个方法等【定义了切面工作在哪里做】

④切面( Aspect ):通知和切点的结合【面】

⑤引入( Introduction ):允许我们向现有的类添加新方法或属性

⑥织入( Weaving ):把切面应用到目标对象并创建新的代理对象的过程【切面在指定的连接点被织入到目标对象中】

通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知

Spring对AOP的支持(包括代理模式以及示例demo)

  1. 基于代理的经典 Spring AOP ;
  2. 纯 POJO 切面;
  3. @AspectJ 注解驱动的切面;
  4. 注入式 AspectJ 切面(适用于 Spring 各版本)。
  • 静态代理
    AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。

    为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

    比如,有两个有钱人张三和李四在杭州有一套房子要租出去,只有找到租客才能收租金,李四找了中介帮他,张三自己找人,其中的中介就起到代理的作用,李四可以不再去管租房的事情而是每个月收收房租做个快乐的房东,张三就不一样了,他必须自己找到租客。

    下面demo有关于静态代理的代码实现。

  • 动态代理
    Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

    • JDK动态代理
      面向接口生成代理,原理就是类加载器根据接口,在虚拟机内部创建接口实现类
      Proxy.newProxyInstance(classloader,interfaces[], invocationhandler );
      invocationHandler 通过invoke()方法反射来调用目标类中的代码。
      1.创建目标业务对象的引用;
      2.使用目标业务对象类加载器和接口,在内存中创建代理对象;
      3.实现InvocationHandler接口。
      Java动态代理InvocationHandler和Proxy学习笔记
    • CGLIB动态代理
      使用范围:对于不适用接口的业务类,无法使用JDK动态代理。
      原理:CGLIB采用非常底层的字节码技术,可以为一个类创建子类,解决无接口类的代理类问题。
      通过实现MethodInterceptor接口重写intercept方法实现拦截与代理增加;通过invokeSuper()方法反射调用目标类中的代码。
      1.使用cglib自带的字节码增强器Enhancer创建真实业务类的子类;
      2.将委托类设置成父类;
      3.设置回调函数、拦截器。
      cglib源码分析
  • 具体实现demo:
    Spring对AOP切面支持实现及示例demo(代理模式实现、注解驱动、注入式)

Spring JDBC

概念

Spring通过抽象JDBC访问并提供一致的API来简化JDBC编程的工作量。我们只需要声明SQL、调用合适的Spring JDBC框架API、处理结果集即可。

与传统JDBC的区别

传统JDBCSpring JDBC
获取JDBC连接获取JDBC连接
声明sql声明sql
预编译sql预编译sql
执行sql执行sql
处理结果集处理结果集
释放结果集释放结果集
释放Statement释放Statement
提交事务提交事务
处理异常并回滚事务处理异常并回滚事务
释放JDBC连接释放JDBC连接
--------------------------------------------------------------------------------------
缺点优点
1.冗长,重复1.简单、简洁
2.显示事务控制2.Spring事务控制
3.每个步骤不可获取3.只做需要做的
4.显示处理受检查异常4.一致的非检查异常体系

Spring JDBC的三种工作模式

  • JDBC模板方式: Spring JDBC框架提供以下几种模板类来简化JDBC编程,实现GoF模板设计模式,将可变部分和非可变部分分离,可变部分采用回调接口方式由用户来实现:如JdbcTemplate、NamedParameterJdbcTemplate、SimpleJdbcTemplate。
  • 关系数据库操作对象化方式: Spring JDBC框架提供了将关系数据库操作对象化的表示形式,从而使用户可以采用面向对象编程来完成对数据库的访问;如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等类。这些类的实现一旦建立即可重用并且是线程安全的。
  • SimpleJdbc方式: Spring JDBC框架还提供了SimpleJdbc方式来简化JDBC编程,SimpleJdbcInsert 、 SimpleJdbcCall用来简化数据库表插入、存储过程或函数访问。

Spring JDBC还提供了一些强大的工具类,如DataSourceUtils来在必要的时候手工获取数据库连接等。

Spring JDBC模块组成

在这里插入图片描述

  • support包: 提供将JDBC异常转换为DAO非检查异常转换类、一些工具类如JdbcUtils等。
  • datasource包: 提供简化访问JDBC 数据源(javax.sql.DataSource实现)工具类,并提供了一些DataSource简单实现类从而能使从这些DataSource获取的连接能自动得到Spring管理事务支持。
  • core包: 提供JDBC模板类实现及可变部分的回调接口,还提供SimpleJdbcInsert等简单辅助类。
  • object包: 提供关系数据库的对象表示形式,如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等类,该包是基于core包JDBC模板类实现。

Spring JDBC模板实战

  1. 引入jar包,也可以自己选择其他的连接池和数据库驱动比如DBCP,C3P0等。
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>5.1.5.RELEASE</version>
</dependency>
		
<!-- 阿里巴巴数据源包 -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.1.10</version>
</dependency>

<!-- mysql驱动包 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.35</version>
</dependency>
  1. 创建db.properties数据库属性文件,注意此处配置只适用与Mysql5.7及以下版本,Mysql5.8版本配置与之前的版本有区别
validationQuery=SELECT 1 FROM DUAL

jdbc_driverClassName=com.mysql.jdbc.Driver
jdbc_url=jdbc\:mysql\://192.168.1.15\:3311/test?rewriteBatchedStatements\=true&amp;useUnicode\=true&amp;characterEncoding\=utf-8

jdbc_username=root
jdbc_password=123456

  1. 创建springjdbc.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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">

	<!-- 属性文件的注入方法:Bean方法 -->
	<!-- <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location" value="classpath:db.properties"></property>
	</bean> -->
	<!-- 属性文件的注入方法:Context方法 -->
	<context:property-placeholder location="classpath:db.properties" />
	
	<bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter">
		<property name="statementExecutableSqlLogEnable" value="false" />
		<property name="resultSetLogEnabled" value="false" />
		<property name="statementPrepareAfterLogEnabled" value="false" />
	</bean>

	<bean id="dataSourceDefault" 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}" />
		<!-- 初始化连接大小 -->
		<property name="initialSize" value="2" />
		<!-- 连接池最大使用连接数量 -->
		<property name="maxActive" value="60" />
		<!-- 连接池最小空闲 -->
		<property name="minIdle" value="0" />
		<!-- 获取连接最大等待时间 -->
		<property name="maxWait" value="60000" />
		<property name="validationQuery" value="${validationQuery}" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<property name="testWhileIdle" value="true" />
		<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="25200000" />
		<!-- 打开removeAbandoned功能 -->
		<property name="removeAbandoned" value="true" />
		<!-- 1800秒,也就是30分钟 -->
		<property name="removeAbandonedTimeout" value="1800" />
		<!-- 关闭abanded连接时输出错误日志 -->
		<property name="logAbandoned" value="true" />
		<!-- 打印日志 -->
		<property name="proxyFilters">
			<list>
				<ref bean="log-filter" />
			</list>
		</property>
	</bean>

	<!-- Spring提供的jdbcTemplate模板 -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSourceDefault"></property>
	</bean>
</beans>
  1. 在目标数据库创建表
create table jdbc_test(
	id varchar(20) NOT NULL PRIMARY KEY,
  text varchar(100) DEFAULT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 创建测试类
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

public class JdbcTest {
	public static void main(String[] args) {
		ApplicationContext ac = new ClassPathXmlApplicationContext("/springjdbc.xml");
		JdbcTemplate jdbc = ac.getBean(JdbcTemplate.class);
		String sql = "insert into jdbc_test values(?,?)";
		for (int i = 0; i < 20; i++) {
			//新建
			jdbc.update(sql,i,"test"+i);
		}
	}
}

SpringBean

Bean的生命周期

可以简单描述bean的生命周期:

  1. 实例化Bean
  2. 依赖注入
  3. 处理Aware接口
  4. BeanPostProcessor(自定义处理)
  5. init-method(初始化)
  6. DisposableBean(清理阶段)
  7. destroy-method(销毁)

待更新
若容器实现了流程图中涉及的接口,程序将按照以上流程进行。需要我们注意的是,这些接口并不是必须实现的,可根据自己开发中的需要灵活地进行选择,没有实现相关接口时,将略去流程图中的相关步骤。

分类类型所包含方法
Bean自身的方法配置文件中的init-method和destroy-method配置的方法、Bean对象自己调用的方法
Bean级生命周期接口方法BeanNameAware、BeanFactoryAware、InitializingBean、DiposableBean等接口中的方法
容器级生命周期接口方法InstantiationAwareBeanPostProcessor、BeanPostProcessor等后置处理器实现类中重写的方法

SpringBean生命周期详解

提供后初始化和预破坏方法的三种方式

创建实体类

public class Employee {
	private String name;
	 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}

}

@ PostConstruct,@ PreDestroy注释

将定义的init-method用@ PostConstruct注释,destroy-method用@PreDestroy注释

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class MyService {
	
	private Employee employee;

	public Employee getEmployee() {
		return employee;
	}

	public void setEmployee(Employee employee) {
		this.employee = employee;
	}
	

	@PostConstruct
	public void init(){
		System.out.println("MyService init method called");
		if(employee.getName() == null){
			employee.setName("woc");
		}
	}
	
	public MyService(){
		System.out.println("MyService no-args constructor called");
	}
	
	@PreDestroy
	public void destory(){
		System.out.println("MyService destroy method called");
	}
}

配置文件

<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor" />
<bean name="myService" class="beanLifeCycle.MyService" >
	<property name="employee" ref="employee"></property>
</bean>

InitializingBean,DisposableBean接口

实现post-init和pre-destroy方法的接口

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class EmployeeService implements InitializingBean,DisposableBean{
	private Employee employee;
	 
	public Employee getEmployee() {
		return employee;
	}
 
	public void setEmployee(Employee employee) {
		this.employee = employee;
	}
	
	public EmployeeService(){
		System.out.println("EmployeeService no-args constructor called");
	}

	
	public void destroy() throws Exception {
		System.out.println("EmployeeService Closing resources");
	}

	public void afterPropertiesSet() throws Exception {
		System.out.println("EmployeeService initializing to dummy value");
		if(employee.getName() == null){
			employee.setName("66666666");
		}
	}

}

配置文件

<bean name="employeeService" class="beanLifeCycle.EmployeeService">
	<property name="employee" ref="employee"></property>
</bean>

自定义post-init,pre-destroy

自定义方法在配置文件中配置

public class MyEmployeeService{

	private Employee employee;

	public Employee getEmployee() {
		return employee;
	}

	public void setEmployee(Employee employee) {
		this.employee = employee;
	}
	
	public MyEmployeeService(){
		System.out.println("MyEmployeeService no-args constructor called");
	}

	//pre-destroy method
	public void destroy() throws Exception {
		System.out.println("MyEmployeeService Closing resources");
	}

	//post-init method
	public void init() throws Exception {
		System.out.println("MyEmployeeService initializing to dummy value");
		if(employee.getName() == null){
			employee.setName("daniel");
		}
	}
}

配置文件

<bean name="myEmployeeService" class="beanLifeCycle.MyEmployeeService"
		init-method="init" destroy-method="destroy">
	<property name="employee" ref="employee"></property>
</bean>

注意点

  • bean初始化的顺序与spring bean配置文件中定义的顺序相同。
  • Spring Context首先调用no-args构造函数来初始化bean对象,然后调用post-init方法。
  • 仅当使用post-init方法执行正确初始化所有spring bean时才返回上下文。
  • 当上下文被关闭时,bean按照它们被初始化的相反顺序被销毁,即以LIFO(后进先出)顺序。

Aware接口

Spring Aware接口类似于具有回调方法和实现观察者设计模式的servlet监听器。
一些重要的Aware接口是:

  • ApplicationContextAware - 注入ApplicationContext对象,示例用法是获取bean定义名称的数组。
  • BeanFactoryAware - 注入BeanFactory对象,示例用法是检查bean的范围。
  • BeanNameAware - 知道配置文件中定义的bean名称。
  • ResourceLoaderAware - 要注入ResourceLoader对象,示例用法是获取类路径中文件的输入流。
  • ServletContextAware - 在MVC应用程序中注入ServletContext对象,示例用法是读取上下文参数和属性。
  • ServletConfigAware - 在MVC应用程序中注入ServletConfig对象,示例用法是获取servlet配置参数。
import java.util.Arrays;
 
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
 
public class MyAwareService implements ApplicationContextAware,
		ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware,
		BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware {
 
	@Override
	public void setApplicationContext(ApplicationContext ctx)
			throws BeansException {
		System.out.println("setApplicationContext called");
		System.out.println("setApplicationContext:: Bean Definition Names="
				+ Arrays.toString(ctx.getBeanDefinitionNames()));
	}
 
	@Override
	public void setBeanName(String beanName) {
		System.out.println("setBeanName called");
		System.out.println("setBeanName:: Bean Name defined in context="
				+ beanName);
	}
 
	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		System.out.println("setBeanClassLoader called");
		System.out.println("setBeanClassLoader:: ClassLoader Name="
				+ classLoader.getClass().getName());
	}
 
	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		System.out.println("setResourceLoader called");
		Resource resource = resourceLoader.getResource("classpath:spring.xml");
		System.out.println("setResourceLoader:: Resource File Name="
				+ resource.getFilename());
	}
 
	@Override
	public void setImportMetadata(AnnotationMetadata annotationMetadata) {
		System.out.println("setImportMetadata called");
	}
 
	@Override
	public void setEnvironment(Environment env) {
		System.out.println("setEnvironment called");
	}
 
	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		System.out.println("setBeanFactory called");
		System.out.println("setBeanFactory:: employee bean singleton="
				+ beanFactory.isSingleton("employee"));
	}
 
	@Override
	public void setApplicationEventPublisher(
			ApplicationEventPublisher applicationEventPublisher) {
		System.out.println("setApplicationEventPublisher called");
	}
 
}

Bean的作用域

概念

什么是作用域呢?即“scope”,在面向对象程序设计中一般指对象或变量之间的可见范围。而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围。

Spring提供“singleton”和“prototype”两种基本作用域,另外提供“request”、“session”、“global session”三种web作用域;Spring还允许用户定制自己的作用域。

默认情况下,Spring容器装配的Bean都是单例的,也就是说,不管什么情况下,在同一应用中通过Spring容器获取的都是同一个对象,也就导致了这个对象携带了很多可变的属性,有时候会很不方便。
比如:我们通过ApplicationContext先后获取BaseBean进行设值和取值,可以看到他们是同一个对象

@Component
public class BaseBean {
    private String name="BaseBean";
}
 
public class ScopeTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentConfig.class);
        
        //从Spring容器获取BaseBean并进行属性设置
        BaseBean baseBean = applicationContext.getBean(BaseBean.class);
        baseBean.setName("My Name");
        
        //从Spring容器获取BaseBean并进行属性比对
        BaseBean baseBean1 = applicationContext.getBean(BaseBean.class);
        System.out.println(baseBean.equals(baseBean1));
        System.out.println(baseBean1.getName());
    }
}
 
测试结果:
true
My Name

分类

  1. singleton:指“singleton”作用域的Bean只会在每个Spring IoC容器中存在一个实例,而且其完整生命周期完全由Spring容器管理。对于所有获取该Bean的操作Spring容器将只返回同一个Bean。
	GoF单例设计模式指“保证一个类仅有一个实例,并提供一个访问它的全局访问点”,
	介绍了两种实现:通过在类上定义静态属性保持该实例和通过注册表方式。

在这里插入图片描述

  1. prototype:即原型,指每次向Spring容器请求获取Bean都返回一个全新的Bean,相对于“singleton”来说就是不缓存Bean,每次都是一个根据Bean定义创建的全新Bean。
	GoF原型设计模式,指用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

在这里插入图片描述

  1. Web应用中的作用域
    配置方式和基本的作用域相同,只是必须要有web环境支持,并配置相应的容器监听器或拦截器从而能应用这些作用域。

    • request作用域:表示每个请求需要容器创建一个全新Bean。比如提交表单的数据必须是对每次请求新建一个Bean来保持这些表单数据,请求结束释放这些数据。
    • session作用域:表示每个会话需要容器创建一个全新Bean。比如对于每个用户一般会有一个会话,该用户的用户信息需要存储到会话中,此时可以将该Bean配置为web作用域。
    • globalSession:类似于session作用域,只是其用于portlet环境的web应用。如果在非portlet环境将视为session作用域。
  2. 自定义作用域

【第三章】 DI 之 3.4 Bean的作用域 ——跟我学spring3

限制Bean的作用域

Ⅰ、在自动化配置中,我们通过@Scope结合@Component注解限制被注解Bean的作用域

如下我们将BaseBean的作用域设置为原型作用域,即每次获取都会重新创建一个新的BaseBean

@Component
@Scope("prototype")
public class BaseBean {
    private String name="BaseBean";
}
 
//执行ScopeTest测试结果如下:
false //baseBean和baseBean1不是同一个BaseBean
BaseBean

其实,Spring将Bean的作用域常量封装在ConfigurableBeanFactory和WebApplicationContext两个类中,其代表常量如下:

  • ConfigurableBeanFactory.SCOPE_SINGLETON =“singleton”;
  • ConfigurableBeanFactory.SCOPE_PROTOTYPE =“prototype”;
  • WebApplicationContext.SCOPE_SESSION = “session”;
  • WebApplicationContext.SCOPE_REQUEST = “request”;

Ⅱ、在JavaConfig显式配置中,我们通过@Scope注解结合@Bean注解限制被注解Bean的作用域

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public BaseBean baseBean{
    return new BaseBean();
}

Ⅲ、在XML显式配置中,我们通过限制bean节点的scope属性限制装配bean的作用域

<bean id="baseBean" class="com.my.spring.bean.BaseBean" scope="prototype">
     <constructor-arg name="name" value="baseBeanOne"></constructor-arg>
</bean>

Spring事务管理

概述

事务是一系列操作组成的工作单元,该工作单元内的操作是不可分割的,即要么所有操作都
做,要么所有操作都不做,这就是事务。

举个例子:
比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱;然后ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。
事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
在企业级应用程序开发中,事务管理必不可少的技术,用来确保数据的完整性和一致性。

事务的四个特性:ACID

  • 原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做;
  • 一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态。
  • 隔离性(Isolation):并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;
  • 持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。

隔离级别与并发问题

隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。因此在实际项目开发中为了考虑并发性能一般使用提交读隔离级别,它能避免丢失更新和脏读,尽管不可重复读和幻读不能避免,但可以在可能出现的场合使用悲观锁或乐观锁来解决这些问题。

隔离级别脏读不可重复读幻读
未提交读(Read uncommitted)可能可能可能
已提交读(Read committed)不可能可能可能
可重复读(Repeatable read)不可能不可能可能
序列化(Serializable)不可能不可能不可能

并发问题

当有多个事务并发执行时,并发执行就可能遇到问题,目前常见的问题如下:

  • 丢失更新:两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的;
  • 脏读:一个事务看到了另一个事务未提交的更新数据;
  • 不可重复读:在同一事务中,多次读取同一数据却返回不同的结果;也就是有其他事务更改了这些数据;
  • 幻读:一个事务在执行过程中读取到了另一个事务已提交的插入数据;即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样。
ps:不可重复读与幻读的区别
不可重复读的重点是修改: 同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 。
幻读的重点在于新增或者删除: 同样的条件, 第1次和第2次读出来的记录数不一样 。

隔离级别

  • 未提交读(Read Uncommitted):最低隔离级别,一个事务能读取到别的事务未提交的更新数据,很不安全,可能出现丢失更新、脏读、不可重复读、幻读;
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读);
  • 可重复读(Repeatable Read):保证同一事务中先后执行的多次查询将返回同一结果,不受其他事务影响,InnoDB默认级别,在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读;
  • 序列化(Serializable):最高隔离级别,性能最差,不允许事务并发执行,而必须串行化执行,最安全,每次读都需要获得表级共享锁,读写相互都会阻塞。
ps:InnoDB解决幻读的方法:基于MVCC(多版本并发控制)
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,
这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 

Spring中七种事务传播行为

事务传播行为类型说明
保证多个操作在同一个事务中---------------------------------------------------------------------------------
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
保证多个操作不在同一个事务中---------------------------------------------------------------------------------
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
嵌套式事务---------------------------------------------------------------------------------
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

事务传播行为代码验证

Spring事务管理(详解+实例)

Spring原理

转自:Spring框架的基本原理分析

示例

讲原理前,先看个例子:
小汽车类Car

package com.example;

public class Car {
    private String name;
    private double price;

    // 省略getter,setter

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

    public void carInfo() {
        System.out.println("i have this car: " + name);
    }
}

汽车拥有者类:Person

package com.example;

public class Person {
    private String name;
    private int age;
    private Car car;

     // 省略getter,setter

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

Spring配置文件spring-config.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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="car" class="com.example.Car">
        <property name="name" value="hongqi"/>
        <property name="price" value="1000000.0"/>
    </bean>

    <bean id="person" class="com.example.Person">
        <property name="name" value="zhaangsan"/>
        <property name="age" value="27"/>
        <property name="car" ref="car"/>
    </bean>
</beans>

测试类SpringMain

package com.example;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringMain {
    public static void main(String[] args) {
        // 创建IOC容器
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 通过getBean获取Car的实例,这里利用了反射
        Car car = (Car) context.getBean("car");
        car.carInfo();

        System.out.println(car.toString());
        Person person = (Person) context.getBean("person");
        System.out.println(person.toString());
    }
}

执行结果:

i have this car: hongqi
Car{name='hongqi', price=1000000.0}
Person{name='zhaangsan', age=27, car=Car{name='hongqi', price=1000000.0}}

原理分析

当执行new ClassPathXmlApplicationContext(“spring-config.xml”)这个动作时,Spring容器也即IOC容器随即创建。容器创建时,加载了保存所有bean信息的配置文件spring-config.xml,此时bean被创建,并保存到了内存中;随即通过context.getBean(“car”);获取相应的bean的实例,这里的car是配置文件中< bean id=”car” …>。

在执行:

ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

时,就已经将对应的属性值设置到对象中了,这里调用了setter方法或者构造器等。
这里我们来简单分析下,如何利用反射获取到了实例。

在Spring配置文件spring-config.xml中:

  <bean id="car" class="com.example.Car">
        <property name="name" value="hongqi"/>
        <property name="price" value="1000000.0"/>
  </bean>

首先容器读取到了< bean … />节点,

1.读取id属性值,得到字符串“car”:

String idStr = "car";

2. 读取class属性,得到全限定名字符串“com.example.Car”:

String classStr = "com.example.Car";

3. 利用反射,通过全限定名获取Class对象:

Class<?> clz = Class.forName(classStr);

4. 接着实例化对象

Object obj = clz.newInstance();

5. 加入到Spring容器中

// springContainer --> Map
springContainer.put(idStr, obj);

而若一个类需要另一个类时,如下:

    <bean id="person" class="com.example.Person">
        ...
        <property name="car" ref="car"/>
    </bean>

解析< property …/>元素
1. 获取name属性值car:

String nameStr = "car";

2. 获取ref属性值car:

String refStr = "car";

3. 生成将要调用setter方法名 :

String setterName = "set" + nameStr.substring(0, 1).toUpperCase() + nameStr.substring(1);

4. 获取Spring容器中名为refStr的Bean:

Object carBean = springContainer.get(refStr);

5. 获取setter方法的Method类,此处的clz是前面实例通过Class.forName()获得的类对象
下面是Person类中的setCar(Car)方法

public void setCar(Car car) {
    this.car = car;
}

通过反射,第一个参数是方法名,第二个参数是方法参数类型

Method setter = clz.getMethod(setterName, carBean.getClass());

6. 调用invoke()方法,此处的obj是刚才反射代码得到的Object对象

public Object invoke(Object obj, Object... args){...}

反射方法invoke中的参数,
obj:从中调用底层方法的对象(简单的说就是调用谁的方法用谁的对象);
args:用于方法调用的参数,所以:

setter.invoke(obj, carBean); 

到此,Spring框架原理基本介绍完毕。

常问面试题

Spring面试底层原理的那些问题,你是不是真的懂Spring?
Spring常见面试题总结(超详细回答)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值