Spring

Spring

1.什么是框架:
框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。
简而言之,框架其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别
人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。

2.Spring是什么:
Spring是分层的 Java SE/EE应用 full-stack 轻量级开源框架,以 IOC(Inverse Of Control:反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层 SpringMVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。

3.Spring的优点:

  • 方便解耦,简化开发
    通过 Spring提供的 IoC容器,可以将对象间的依赖关系交由 Spring进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

  • AOP 编程的支持
    通过 Spring的 AOP 功能,方便进行面向切面的编程,许多不容易用传统OOP 实现的功能可以通过 AOP 轻松应付。声明式事务的支持可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。

  • 方便程序的测试
    可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。

  • 方便集成各种优秀框架
    Spring可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持。

  • 低降低 JavaEE API 的使用难度
    Spring对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的
    使用难度大为降低。

  • Java 源码是经典学习范例
    Spring的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对Java 设计模式灵活运用以及对 Java技术的高深造诣。它的源代码无意是 Java 技术的最佳实践的范例。

4.IOC的概念和作用
i.什么是程序的耦合:
耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。划分模块的一个准则就是高内聚低耦合。
耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则:如果模块间必须存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合。
ii.工厂模式解耦:
在实际开发中我们可以把三层的对象都使用配置文件配置起来,当启动服务器应用加载的时候,让一个类中的方法通过读取配置文件,把这些对象创建出来 并存起来。在接下来的使用的时候,直接拿过来用就好了。那么,这个读取配置文件,创建和获取三层对象的类就是工厂。
iii.控制反转IOC-Inversion Of Control
在应用加载时,创建一个 Map,用于存放三层对象。我们把这个 map 称之为 容器.
工厂就是负责给我们从容器中获取指定对象的类。这时候我们获取对象的方式发生了改变。
原来:
我们在获取对象时,都是采用 new 的方式。是主动的。
现在:
我们获取对象时,同时跟工厂要,有工厂为我们查找或者创建对象。是被动的。
这种被动接收的方式获取对象的思想就是控制反转,它是 spring 框架的核心之一。

1.Spring的IOC
spring中最重要的一个组件就是核心容器。
之前的代码中出现的一些问题
1.程序的耦合
耦合:程序间的依赖关系
包括:
类之间的依赖
方法间的依赖
解耦:
降低程序间的依赖关系
实际开发中:
应该做到:编译期不依赖,运行时才依赖。
解耦的思路:
第一步:使用反射来创建对象,而避免使用new关键字。
第二步:通过读取配置文件来获取要创建的对象全限定类名

工厂类中,使用配置文件获取全类名,使用反射创建对象。

一个创建Bean对象的工厂
Bean:在计算机英语中,有可重用组件的含义。
JavaBean:用java语言编写的可重用组件。javabean > 实体类
它就是创建我们的service和dao对象的。
第一个:需要一个配置文件来配置我们的service和dao配置的内容:唯一标识=全限定类名(key=value)
第二个:通过读取配置文件中配置的内容,反射创建对象我的配置文件可以是xml也可以是properties

public class BeanFactory {
    //定义一个Propeties对象
    private static Properties props;
    //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
    private static Map<String,Object> beans;

    //使用静态代码块为Properties对象赋值
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
            //实例化容器
            beans = new HashMap<String,Object>();
            //取出配置文件中所有的Key
            Enumeration keys = props.keys();
            //遍历枚举
            while (keys.hasMoreElements()){
                //取出每个Key
                String key = keys.nextElement().toString();
                //根据key获取value
                String beanPath = props.getProperty(key);
                //反射创建对象bean
                Object value = Class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }
        }catch(Exception e){
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }

    /**
     * 根据bean的名称获取对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        //从map集合中获取bean
        return beans.get(beanName);
    }
}

        问题:
            如果每一次都新创建一个对象,对内存比较浪费。对于dao或者service来说,没有必要多次创建对象。
        解决方案:
            只创建一次对象,如果再次获取对象,该对象以前创建过,就直接复用。所以我们必须创建一个容器把已有对象存储起来,供以后复用。
        注意:一定要使用容器进行bean的存储


    IOC是什么:
        控制反转(Inversion of Control)
            控制指的就是对象的创建权。把创建对象的权力交给框架,是框架的重要特征,并非面向对象编程的专用术语。他包括依赖注入(Dependency Injection)和依赖查找(Dependency Lookup)。

    IOC的作用:IOC只是从对象创建和管理的角度对项目进行重构,并没有影响功能。
        降低类与类之间的耦合,在创建对象时无需考虑实现类的问题。
        削减计算机程序的耦合(解除我们代码中的依赖关系)。

    IOC的配置:
        创建bean的方式
            1.使用构造创建
                bean标签中使用class属性指定全类名,此时使用默认无参构造创建对象。
                    如果使用有参构造,在bean标签体内使用constructor-arg标签配置构造参数的指定
                        <constructor-arg name="abc" value="abcd"></constructor-arg>
            2.使用非静态工厂创建
                在bean标签中,使用factory-bean属性指定容器中的另一个bean的id,用于指定工厂对象。使用factory-method指定工厂对象中的方法,用于创建当前对象。
                        参数使用<constructor-arg>指定
            3.使用静态工厂创建
                在bean标签中,使用class属性指定静态工厂类的全类名,使用 factory-method指定静态方法的名称
                        参数使用<constructor-arg>指定
         bean标签的scope属性
            创建bean的时候,默认是单例的。可以通过此属性修改这个默认配置
            singleton:单例的(默认值)
            prototype:多例的

<!--把对象的创建交给spring来管理-->
第一种方式:使用默认无参构造函数
<!--在默认情况下:它会根据默认无参构造函数来创建类对象。如果 bean 中没有默认无参构造函数,将会创建失败。-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"/>

第二种方式:spring  管理静态工厂- 使用静态工厂的方法创建对象
/**
* 模拟一个静态工厂,创建业务层实现类
*/
public class StaticFactory {
    public static IAccountService createAccountService(){
    return new AccountServiceImpl();
    }
}
<!-- 此种方式是:使用 StaticFactory 类中的静态方法 createAccountService 创建对象,并存入 spring 容器
    id 属性:指定 bean 的 id,用于从容器中获取
    class 属性:指定静态工厂的全限定类名
    factory-method 属性:指定生产对象的静态方法
-->
<bean id="accountService" class="com.itheima.factory.StaticFactory" factory-method="createAccountService"></bean>

第三种方式:spring  管理实例工厂- 使用实例工厂的方法创建对象
/**
* 模拟一个实例工厂,创建业务层实现类
* 此工厂创建对象,必须现有工厂实例对象,再调用方法
*/
public class InstanceFactory {
    public IAccountService createAccountService(){
    return new AccountServiceImpl();
    }
}
<!-- 此种方式是:先把工厂的创建交给 spring 来管理。然后在使用工厂的 bean 来调用里面的方法
    factory-bean 属性:用于指定实例工厂 bean 的 id。
    factory-method 属性:用于指定实例工厂中创建对象的方法。
-->
<bean id="instancFactory" class="com.itheima.factory.InstanceFactory"></bean>
<bean id="accountService"    factory-bean="instancFactory"    factory-method="createAccountService"></bean>

IOC中 bean标签和管理对象细节:
    作用:
用于配置对象让 spring 来创建的。
默认情况下它调用的是类中的无参构造函数。如果没有无参构造函数则不能创建成功。
属性:
id:给对象在容器中提供一个唯一标识。用于获取对象。
class:指定类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数。
scope:指定对象的作用范围。
* singleton :默认值,单例的.
* prototype :多例的.
* request  :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中.
* session  :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中.
* global session  :WEB 项目中,应用在 Portlet 环境.如果没有 Portlet 环境那么
globalSession 相当于 session.
init-method:指定类中的初始化方法名称。
destroy-method:指定类中销毁方法名称。


   bean对象的作用范围
        bean标签的scope属性:
        作用:用于指定bean的作用范围
        取值: 常用的就是单例的和多例的
            singleton:单例的(默认值)
            prototype:多例的
            request:作用于web应用的请求范围
            session:作用于web应用的会话范围
            global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session
<bean id="AccountService" class="com.Muma.service.AccountServiceimpl" scope="prototype"></bean> 

    bean对象的生命周期
        单例对象
            出生:当容器创建时对象出生
            活着:只要容器还在,对象一直活着
            死亡:容器销毁,对象消亡
            总结:单例对象的生命周期和容器相同
        多例对象
            出生:当我们使用对象时spring框架为我们创建
            活着:对象只要是在使用过程中就一直活着。
            死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收

使用spring容器时,如果是单例的对象创建,我们会采取立即加载的方式,在解析配置文件时创建对象放入容器中
如果是多例对象,我们会采取延迟加载的方式。什么时候获取对象,什么时候创建对象。
单例对象创建时,可以直接初始化时创建,也可以在使用时判断创建。
使用时创建(懒加载)会导致线程安全问题,为了解决要加同步,会降低性能。所以我们使用初始加载的方式。



public class Servlet {
    public static void main(String[] args) {
        //传统方式  使用 new
        //AccountService accountService = new AccountServiceimpl();
        //使用工厂模式
        AccountService accountService = (AccountService) BeanFactory.getBean("AccountService");
        System.out.println("accountService = " + accountService);
        accountService.saveAccount();
    }
}

public class Servlet {
    public static void main(String[] args) {
        //1.获取核心容器对象
        //ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        AccountService as  = (AccountService)ac.getBean("AccountService");
        as.saveAccount();
        //手动关闭容器
        ac.close();
    }
}

BeanFactory 和 和 ApplicationContext的区别:
BeanFactory 才是 Spring 容器中的顶层接口。
ApplicationContext 是它的子接口。
BeanFactory 和 ApplicationContext 的区别:
创建对象的时间点不一样。
ApplicationContext:只要一读取配置文件,默认情况下就会创建对象。
BeanFactory:什么使用什么时候创建对象。

ApplicationContext  接口 的实现类:
 ClassPathXmlApplicationContext :
它是从类的根路径下加载配置文件 推荐使用这种
FileSystemXmlApplicationContext :
它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置。
AnnotationConfigApplicationContext:
当我们使用注解配置容器对象时,需要使用此类来创建 spring 容器。它用来读取注解。



使用注解配置完成IOC
    注解要生效,需要在主配置文件中指定扫描的包
    告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名称为context名称空间和约束中
        <context:component-scan base-package="com.itheima.dao"></context:component-scan>
            base-package:在此包及其子包下的所有类都会被扫描,如果有spring的注解就会生效。

用于创建bean   相当于:<bean id="" class="">
        @Component("accountService")
            作用:
                把资源让 spring 来管理。相当于在 xml 中配置一个 bean。
            属性:
                value:指定 bean 的 id。如果不指定 value 属性,默认 bean 的 id 是当前类的类名。首字母小写。
        @Controller    web表现层
        @Service    业务逻辑层
        @Repository    持久层
        他们三个注解都是针对一个的衍生注解,他们的作用及属性都是一模一样的。他们只不过是提供了更加明确的语义化。
细节:如果注解中有且只有一个属性 要赋值时是 ,且名称是 value ,value  在赋值是可以不写。

用于注入   相当于:<property name="" ref=""> 或 <property name="" value="">
        @Autowired
            自动注入
            首先去容器中按照要注入的属性的类型查找。
            如果有唯一一个值匹配,就直接注入
            如果有多个值匹配,查找bean的id和属性变量名相同的bean,如果有,直接注入。如果没有,直接报错
            出现位置:可以是变量上,也可以是方法上
                 细节:在使用注解注入时,set方法就不是必须的了。
                 注入数据时,如果是list(数组、set)集合,自动注入时会把容器中所有符合list泛型的数据全部注入到list集合中
                 如果是Map集合,自动注入时,会把所有符合map值的泛型的数据全部存入map,key就是配置bean时指定的id。key的类型必须是String    
                 如果自动注入时,该值可有可无,可以指定required为false  @Autowired(required = false)
        @Qualifier
            配合Autowired注解指定容器中某个bean的id获取。
            在参数注入时,可以没有Autowired
        @Resource   相当于:<bean id="" class="" scope="">
            可以替代 @Autowired和 @Qualifier组合,但是要使用name属性指定id
        @Value
            可以直接指定基本数据类型进行注入
            可以使用spEL从属性文件中获取值。

设置作用范围   相当于:<bean id="" class="" scope="">
        @Scope
            singleton  单例的(默认值)  prototype  多例的 request   session  globalsession

生命周期   相当于:<bean id="" class="" init-method="" destroy-method="" />
        @PreDestroy   作用:用于指定初始化方法
        @PostConstruct   作用:用于指定销毁方法。


Spring新注解
    1.@Configuration
        作用:
            用于指定当前类是一个 spring 配置类,当创建容器时会从该类上加载注解。获取容器时需要使用
            AnnotationApplicationContext(有@Configuration 注解的类.class)。
        属性:
            value:用于指定配置类的字节码

    @Configuration //通过该注解来表明该类是一个Spring的配置,相当于一个xml文件
    @ComponentScan(basePackages = "cn.itcast.springboot.javaconfig") //配置扫描包
    public class SpringConfig {
   
        @Bean // 通过该注解来表明是一个Bean对象,相当于xml中的<bean>
        public UserDAO getUserDAO(){
            return new UserDAO(); // 直接new对象做演示
        }
    }

    2. @ComponentScan
        作用:
            用于指定 spring 在初始化容器时要扫描的包。作用和在 spring 的 xml 配置文件中的:
            <context:component-scan base-package="com.itheima"/>是一样的。
        属性:
            basePackages:用于指定要扫描的包。和该注解中的 value 属性作用一样。
    3.@Bean
        作用:
            该注解只能写在方法上,表明使用此方法创建一个对象,并且放入 spring 容器。
        属性:
            name:给当前@Bean 注解方法创建的对象指定一个名称(即 bean 的 id)。
    4.@PropertySource
        作用:
            用于加载.properties 文件中的配置。例如我们配置数据源时,可以把连接数据库的信息写到
            properties 配置文件中,就可以使用此注解指定 properties 配置文件的位置。
        属性:
            value[]:用于指定 properties 文件位置。如果是在类路径下,需要写上 classpath:。

        通过@PropertySource可以指定读取的配置文件,通过@Value注解获取值,具体用法:
        @Configuration //通过该注解来表明该类是一个Spring的配置,相当于一个xml文件
        @ComponentScan(basePackages = "cn.itcast.springboot.javaconfig") //配置扫描包
        @PropertySource(value= {"classpath:jdbc.properties"})
        public class SpringConfig {
   
            @Value("${jdbc.url}")
            private String jdbcUrl;


    5.@Import
        作用:
            用于导入其他配置类,在引入其他配置类时可以不用再写@Configuration 注解。当然,写上也没问题。
        属性:
            value[]:用于指定其他配置类的字节码。


基于IOC的案例
    1.xml配置
    不使用注解  把bean全部配置在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">

    <!-- 配置Service -->
    <bean id="AccountService" class="com.Muma.service.AccountServiceimpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="AccountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="AccountDao" class="com.Muma.dao.AccountDaoimpl" >
        <!-- 注入QueryRunner -->
        <property name="Runner" ref="Runner"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="Runner" class="org.apache.commons.dbutils.QueryRunner">
        <!--注入数据源-->
       <constructor-arg name="ds" ref="ds"></constructor-arg>
    </bean>

    <!-- 配置数据源 -->
    <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb2"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

    2.注解和xml配置
        使用注解创建一部分bean对象  在xml中配置一部分bean对象 
        必须在xml配置文件中配置注解扫component-scan

    <!-- 告知spring在创建容器时要扫描的包 -->
    <context:component-scan base-package="com.Muma"></context:component-scan>
    <!--配置QueryRunner-->
    <bean id="Runner" class="org.apache.commons.dbutils.QueryRunner">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="DataSource"></constructor-arg>
    </bean>
    <!-- 配置数据源 -->
    <bean id="DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb2"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

@Service("AccountService")
public class AccountServiceimpl implements AccountService {
    @Autowired
    //@Qualifier("AccountDao")
    private AccountDao accountDao;
    /*在使用注解注入时,set方法就不是必须的了。
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    */
    @Override
    public List<Account> findAccountAll() {
        return accountDao.findAccountAll();
    }
}

    3.纯注解开发配置
    首先要编写一个类,让其替代配置文件作为spring的配置参数的地方。
         @Configuration 此注解标在哪个类上,哪个类就被当作配置类使用。
         @ComponentScan("包名") 告知spring在创建容器时要扫描的包
    如果在配置中,有第三方的JavaBean需要放入容器,此时在第三方源码中没有spring的注解,而我们又需要使用注解配置,此时可以使用工厂方法完成对象的提供
        在工厂方法上使用 @Bean注解配置JavaBean进入容器
            bean的name属性为JavaBean的id
        如果工厂方法有参数,参数会自带一个Autowired注解。可以使用Qualifier注解手动指定bean的id
        在Bean注解编写之后,可以使用Scope指定JavaBean的作用范围。
    在主配置中,可以使用 @Import注解引入其他配置类。
    在主配置中,可以使用 @PropertySource("classpath:jdbcConfig.properties")指定配置文件的资源路径。一般使用类路径


/**
* 该类是一个配置类,它的作用和bean.xml是一样的
//@Configuration
@ComponentScan("com.Muma")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {

}


/**
* 和spring连接数据库相关的配置类
*/
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 用于创建一个Runner对象
     * @param dataSource
     * @return
     */
    @Bean(name = "Runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(@Qualifier("ds") DataSource dataSource){
        return new QueryRunner(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name = "ds")
    public DataSource createDataSource(){
        try{
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;

        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}


关于Spring注解和xml选择的问题:
   注解的优势:
    配置简单,维护方便(我们找到类,就相当于找到了对应的配置)。
   XML的优势:
    修改时,不用改源码。不涉及重新编译和部署。
    

使用spring整合junit(junit要在4.12版本及以上)
    1.让junit创建spring容器
        @RunWith junit的一个注解,通过此注解可以指定一个自定义的运行器
        @RunWith(SpringJUnit4ClassRunner.class)
    2.让junit能够解析spring注解配置。
        @ContextConfiguration 通过此注解指定spring的配置文件或配置类
            classes:用于指定配置类
            locations:用于指定配置文件
            @ContextConfiguration(classes = SpringConfiguration.class)


2.Spring的DI
    依赖注入
        在我们的对象创建过程中,如果对象的属性上有其他对象。把对象中的对象属性的值赋值的过程叫做依赖注入。
        在spring中我们可以采用set方法注入和构造注入的方式。
    依赖注入:
            能注入的数据:有三类
                基本类型和String
                其他bean类型(在配置文件中或者注解配置过的bean)
                复杂类型/集合类型
             注入的方式:有三种
                第一种:使用构造函数提供
                第二种:使用set方法提供
                第三种:使用注解提供

Spring中的依赖注入
    依赖注入:
        Dependency Injection
    依赖关系的管理:
        以后都交给spring来维护
    在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明
    依赖关系的维护:
        就称之为依赖注入。

构造函数注入:
    使用的标签:constructor-arg
    标签出现的位置:bean标签的内部
    标签中的属性
        type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
        index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
        name:用于指定给构造函数中指定名称的参数赋值   (常用的)
        =============以上三个用于指定给构造函数中哪个参数赋值===============================
        value:用于提供基本类型和String类型的数据
        ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象

    优势:在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
    弊端:改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。

<bean id="AccountService1" class="com.Muma.service.AccountServiceimpl1">
    <constructor-arg name="name" value="高慧美"></constructor-arg>
    <constructor-arg name="age" value="18"></constructor-arg>
    <constructor-arg name="birthday" ref="Birthday1"></constructor-arg>
</bean>
<!-- 配置一个日期对象 -->
<bean id="Birthday1" class="java.util.Date"></bean>

set方法注入                更常用的方式
    涉及的标签:property
    出现的位置:bean标签的内部
    标签的属性
        name:用于指定注入时所调用的set方法名称
        value:用于提供基本类型和String类型的数据
        ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
    优势:创建对象时没有明确的限制,可以直接使用默认构造函数
    弊端:如果有某个成员必须有值,则获取对象是有可能set方法没有执行。

<bean id="AccountService2" class="com.Muma.service.AccountServiceimpl2">
    <property name="name" value="高慧玉"></property>
    <property name="age" value="17"></property>
    <property name="birthday" ref="Birthday2"></property>
</bean>
<!-- 配置一个日期对象 -->
<bean id="Birthday2" class="java.util.Date"></bean>

复杂类型的注入/集合类型的注入
    用于给List结构集合注入的标签:
        list array set
    用于个Map结构集合注入的标签:
        map  props
    结构相同,标签可以互换




事务管理
    Connection用于事务管理。
        前提:同一个事务要用同一个Connection对象才能管理。
        为了保证Connection在多次使用时用的是同一个对象,我们用一个在同一个业务中不会改变的对象进行绑定            Connection获取。
        同一个业务中线程是同一个。
    ThreadLocal
        本质上就是一个Map,key就是当前线程,value是指定的泛型类型。
            ThreadLocal<Connection>
        存值:tl.set(value);
            以当前线程为key,指定的value为值,存入map
        取值:tl.get();
            以当前线程为key,从ThreadLocal中获取值
        整个使用过程中,key不需要我们维护,自动拿当前线程。
    事务要添加到业务逻辑层
事务添加之后,方法调用的依赖过多,代码比较臃肿,不利于维护。
    目的:削减方法间的调用依赖。
代理:
    在不破坏原有代码结构的基础上对代码进行扩展,可以添加功能,减少功能甚至直接覆盖原有功能。
    如果扩展不再需要,随时可以切换会原有代码逻辑。
动态代理
    基于接口的代理
        要求:被代理对象必须实现接口。
        JDK的api
            Proxy.newProxyInstance(ClassLoader,Class[] interfaces,InvocationHandler)
    基于子类的代理
        如果被代理对象没有任何接口,此时可以使用这种方式。类不可以被final修饰。
        cglib的api
            Enhancer.create(Class,MethodIntercepter)
                在方法拦截器中,有intercept方法。当代理对象的方法被调用时,实际执行的就是此方法。


SpringAOP
    面向切面编程
        AOP:全称是 Aspect Oriented Programming 即:面向切面编程。
        在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
        简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的
基础上,对我们的已有方法进行增强。
    解决的问题:
            在不修改代码的前提下对一个类的对象进行扩展。便于维护。

    连接点
        在动态代理中,被增强类中的所有方法。
    切入点
        在动态代理中,被增强类中被增强的方法。
    通知
        在动态代理中,切入点被赠强时添加的逻辑。增强代码
    目标对象
        被代理对象(增强前的对象)
    织入
        编写动态代理生成代理类的过程叫做织入。
    代理
        代理对象(增强后的对象)
    切面
        增强的代码加到切入点的什么位置是由开发人员决定,这个位置就叫做切面。切面指的就是增强代码和未增强代码之间的结合关系。


AOP技术点
    动态代理

AOP的配置
    <!--配置AOP-->
        Spring中基于XML的AOP配置步骤
1、把通知Bean也交给spring来管理
2、使用aop:config标签表明开始AOP的配置
3、使用aop:aspect标签表明配置切面
    id属性:是给切面提供一个唯一标识
    ref属性:是指定通知类bean的Id。
4、在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
    aop:before:表示配置前置通知
    method属性:用于指定Logger类中哪个方法是前置通知
    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

切入点表达式的写法:关键字:execution(表达式)
表达式:
    访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
    public void com.Muma.utils.Logger.printLog()
访问修饰符可以省略
    void com.Muma.utils.Logger.printLog()
返回值可以使用通配符,表示任意返回值
    * com.Muma.utils.Logger.printLog()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
    * *.*.*.Logger.printLog()
包名可以使用..表示当前包及其子包
    * *..Logger.printLog()
类名和方法名都可以使用*来实现通配
    * *..*.*()
参数列表:
    可以直接写数据类型:
    基本类型直接写名称 int
    引用类型写包名.类名的方式 java.lang.String
    可以使用通配符表示任意类型,但是必须有参数
    可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
    * *..*.*(..)
实际开发中切入点表达式的通常写法:切到业务层实现类下的所有方法
    * com.Muma.service.impl.*.*(..)
        注意:
        编写切入点表达式时一定不要忘记execution关键字!!!

<!-- 配置Logger类 -->
    <bean id="Logger" class="com.Muma.utils.Logger"></bean>

   <!--配置AOP-->
<aop:config>
    <!--配置切面 -->
    <aop:aspect id="logAdvice" ref="Logger">
        <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
        <aop:before method="printLog" pointcut="execution(* com.Muma.utils.*.*(..))"></aop:before>
    </aop:aspect>
</aop:config>


通知类型
    相对于切入点的位置命名
    前置通知
        before
    后置通知
        after-returning
    异常通知
        after-throwing
    最终通知
        after
    环绕通知
        包含了上述所有通知,同时还包含切入点方式的执行。
        自己通过编码方式实现。类似于动态代理中InvocationHandler中invoke方法的编写方式。
        环绕通知方法需要指定一个参数
            ProceedingJoinPoint
                proceed方法,执行原来被增强前的方法逻辑。可以理解为动态代理时的method.invoke
    如果使用注解方式配置aop,只能使用环绕通知。因为后置和最终顺序时颠倒的。



1.动态代理( CGLIB 与 JDK)
(1)java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用
InvokeHandler来处理。
(2)cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节
码生成子类来处理。
    1.如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
    2.如果目标对象实现了接口,可以强制使用CGLIB实现AOP
    3.如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

2.如何强制使用CGLIB实现AOP
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入<aop:aspectj­autoproxy proxy­target­class=“true”/>

3.JDK动态代理和CGLIB字节码生成的区别
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法因为是继承,所以该类或方法最好不要声明成final


4.Spring 框架中用到了哪些设计模式

    代理模式—在AOP和remoting中被用的比较多。

    单例模式—在spring配置文件中定义的bean默认为单例模式。

    模板方法—用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。

    工厂模式—BeanFactory用来创建对象的实例。

    适配器–spring aop

    装饰器 –spring data hashmapper

    观察者­­ spring 时间驱动模型

    回调–Spring ResourceLoaderAware回调接口

    前端控制器–spring用前端控制器DispatcherServlet对请求进行分发



Spring的事务管理机制实现的原理,就是通过这样一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的 方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。Spring中的AOP实 现更为复杂和灵活,不过基本原理是一致的。


统观spring事务,围绕着两个核心PlatformTransactionManager和TransactionStatus

spring提供了几个关于事务处理的类:
TransactionDefinition //事务属性定义
TranscationStatus //代表了当前的事务,可以提交,回滚。
PlatformTransactionManager这个是spring提供的用于管理事务的基础接口,其下有一个实现的抽象类AbstractPlatformTransactionManager,我们使用的事务管理类例如DataSourceTransactionManager等都是这个类的子类。

一般事务定义步骤:
TransactionDefinition td = new TransactionDefinition();
TransactionStatus ts = transactionManager.getTransaction(td);
try
{ //do sth
  transactionManager.commit(ts);
}
catch(Exception e){transactionManager.rollback(ts);}

spring提供的事务管理可以分为两类:编程式的和声明式的。编程式的,比较灵活,但是代码量大,存在重复的代码比较多;声明式的比编程式的更灵活。
1.编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维
护。
2.声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和 XML 配
置来管理事务。

编程式主要使用transactionTemplate。省略了部分的提交,回滚,一系列的事务对象定义,需注入事务管理对象.
<!-- 定义编程式的事务) -->

void add()
{
    transactionTemplate.execute( new TransactionCallback(){
        pulic Object doInTransaction(TransactionStatus ts)
       { //do sth}
    }
}


<!-- 定义事务管理器(声明式的事务) -->
Spring中基于XML的声明式事务控制配置步骤
1、配置事务管理器
2、配置事务的通知
此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop的
使用tx:advice标签配置事务通知
属性:
id:给事务通知起一个唯一标识
transaction-manager:给事务通知提供一个事务管理器引用
3、配置AOP中的通用切入点表达式
4、建立事务通知和切入点表达式的对应关系
5、配置事务的属性是在事务的通知tx:advice标签的内部

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 配置事务的属性
            isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
            propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
            read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
            timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
            rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
            no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。
    -->
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED" read-only="false"/>
        <tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
    </tx:attributes>
</tx:advice>
<!-- 配置aop-->
<aop:config>
    <!-- 配置切入点表达式-->
    <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
    <!--建立切入点表达式和事务通知的对应关系 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>


Spring中基于注解 的声明式事务控制配置步骤
1、配置事务管理器
2、开启spring对注解事务的支持
3、在需要事务支持的地方使用@Transactional注解


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

<!-- 开启spring对注解事务的支持-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>



围绕Poxy的动态代理 能够自动的提交和回滚事务
org.springframework.transaction.interceptor.TransactionProxyFactoryBeanz中的transactionAttributes属性的Props中一个为key为方法名,支持*,一个为Propagation传播行为类别:

    PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
    PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
    PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
    PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。
    PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。
    PROPAGATION_NESTED--如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

Isolation Level(事务隔离等级): 从名字上来所串行化,可重复读,提交读,未提交读

    1、Serializable:最严格的级别,事务串行执行,资源消耗最大;
    2、REPEATABLE READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
    3、READ COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。

    4、Read Uncommitted:保证了读取过程中不会读取到非法数据。

隔离级别在于处理多事务的并发问题。 并行可以提高数据库的吞吐量和效率,但是并不是所有的并发事务都可以并发运行,这需要查看数据库教材的可串行化条件判断了。
并发中可能发生的3中不讨人喜欢的事情 :

    1: Dirty reads--读脏数据。也就是说,比如事务A的未提交(还依然缓存)的数据被事务B读走,如果事务A失败回滚,会导致事务B所读取的的数据是错误的。
    2: non-repeatable reads--数据不可重复读。比如事务A中两处读取数据-total-的值。在第一读的时候,total是100,然后事务B就把total的数据改成200,事务A再读一次,结果就发现,total竟然就变成200了,造成事务A数据混乱。 即同一个事务读取数据的值是不一样的

    3: phantom reads--幻象读数据,这个和non-repeatable reads相似,也是同一个事务中多次读不一致的问题。但是non-repeatable reads的不一致是因为他所要取的数据集被改变了(比如total的数据),但是phantom reads所要读的数据的不一致却不是他所要读的数据集改变,而是他的条件数据集改变。比如Select account.id where account.name="ppgogo*",第一次读去了6个符合条件的id,第二次读取的时候,由于事务b把一个帐号的名字由"dd"改成"ppgogo1",结果取出来了7个数据。

事务的隔离等级与三种并发问题的杜绝: 以下四者形成了完美的对角线关系

              Dirty reads non-repeatable reads phantom reads
Serializable    不会        不会           不会
Repetable READ  不会        不会           会  
READ Comminted  不会        会             会
Read Uncommitted 会         会             会

对于事务的隔离等级可以在Hibernate.properties文件中或cfg.xml中进行配置,数字分别对应以上的隔离等级类别
<property name=” hibernate.connection.isolation”>4</propert>



在Spring与与Hibernate整合时,可以使用声明式配置AOP来做事务处理org.springframework.orm.hibernate3.HibernateTransactionManager,如下

    <!--事务管理器配置-->  
    <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
       <property name="sessionFactory">  
        <ref local="sessionFactory"/>  
       </property>  
    </bean>  

    <!--AOP 事务配置-->  
    <tx:advice id="txAdvice" transaction-manager="transactionManager">  
          <!-- the transactional semantics... -->  
           <tx:attributes>  
               <!-- all methods starting with 'get' are read-only -->  
               <tx:method name="get*" read-only="true"/>  
               <tx:method name="add*" read-only="false"/>  
               <tx:method name="insert*" read-only="false"/>  
               <tx:method name="update*" read-only="false"/>  
               <tx:method name="del*" read-only="false"/>  
               <tx:method name="audit*" read-only="false"/>  
      
               <!-- other methods use the default transaction settings (see below) -->  
               <tx:method name="*"/>  
            </tx:attributes>  
        </tx:advice>  
        <aop:config>  
            <aop:pointcut id="SysFileOperation" expression="execution(* com.biz.system.SysFilesBiz.*(..))"/>  
            <aop:advisor advice-ref="txAdvice" pointcut-ref="SysFileOperation"/>  
        </aop:config>  

Spring 事务管理创造性的解决了很多以前要用重量级的应用服务器才能解决的事务问题,那么其实现原理一定很深奥吧?可是如果读者仔细研究了Spring事务管理的代码以后就会发现,事务管理其实也是如此简单的事情。这也印证了在本书开头的一句话“重剑无锋、大巧不工”,Spring并没有使用什么特殊的API,它运行的原理就是事务的原理。下面是DataSourceTransactionManager的启动事务用的代码(经简化):


    protected void doBegin(Object transaction, TransactionDefinition definition)
    {
       DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
       Connection con = null;
       try
       {
          if (txObject.getConnectionHolder() == null)
          {
            Connection newCon = this.dataSource.getConnection();
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
          }
          txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
          con = txObject.getConnectionHolder().getConnection();
     
          Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
          txObject.setPreviousIsolationLevel(previousIsolationLevel);
          if (con.getAutoCommit())
          {
              txObject.setMustRestoreAutoCommit(true);
              con.setAutoCommit(false);
          }
          txObject.getConnectionHolder().setTransactionActive(true);
          // Bind the session holder to the thread.
          if (txObject.isNewConnectionHolder())
          {
              TransactionSynchronizationManager.bindResource(getDataSource(),txObject.getConnectionHolder());
          }
        }
        catch (SQLException ex)
        {
           DataSourceUtils.releaseConnection(con, this.dataSource);
           throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
        }
    }


在调用一个需要事务的组件的时候,管理器首先判断当前调用(即当前线程)有没有一个事务,如果没有事务则启动一个事务,并把事务与当前线程绑定。Spring使用TransactionSynchronizationManager的bindResource方法将当前线程与一个事务绑定,采用的方式就是ThreadLocal,这可以从TransactionSynchronizationManager类的代码看出。

    public abstract class TransactionSynchronizationManager
    {
     ……
     private static final ThreadLocal currentTransactionName = new ThreadLocal();
     private static final ThreadLocal currentTransactionReadOnly = new ThreadLocal();
     private static final ThreadLocal actualTransactionActive = new ThreadLocal(); ……
    }


从doBegin的代码中可以看到在启动事务的时候,如果Connection是自动提交的(也就是getAutoCommit()方法返回true)则事务管理就会失效,所以首先要调用setAutoCommit(false)方法将其改为非自动提交的。setAutoCommit(false)这个动作在有的JDBC驱动中会非常耗时,所以最好在配置数据源的时候就将“autoCommit”属性配置为true。


刚学spring,在spring的事务编程中有一些事务的原理问题不明白?
1,一般在单一数据库中,如果手动方式写事务的话,我对事务的理解就是
(1)首先在数据库连接中
con.setAutocommit(false)设置手动提交事务
(2)然后执行sql语句,最后提交。
stmt = con.createStatement();
stmt.executeUpdate("UPDATE user SET age = 18 WHERE id = 'erica'");
con.commit()
2,可是在spring的声明式的事务编程中,我有点不明白,举以下一个例子,但这个例子中,我没法体现上面的事务步骤?首先设置con.setAutocommit(false),然后提交
(1)假设数据源是基于绑定在容器的JNDI上面,并且只有一个数据库
在spring配置文件中,为了演示的方便,我简略了一些代码,有些代码没有写出来,

    <bean id="dataSource"  class="org.springframework.jndi.JndiObjectFactoryBean">
      <property name="jndiName">
        <value>jdbc/sample</value>
      </property>
    </bean>
    <bean id="transactionManager"class="org.springframework.transaction.jta.JtaTransactionManager"/>
    <bean id="userDAO" class="net.xiaxin.dao.UserDAO">
       <property name="dataSource">
          <ref local="dataSource" />
       </property>
       <property name="transactionManager">
         <ref local="transactionManager" />
       </property>
    </bean>



(2)在java的源文件中,是这样写的:

    public class UserDAO {
    private DataSource dataSource;
    private PlatformTransactionManager transactionManager;
    public PlatformTransactionManager getTransactionManager() {
              return transactionManager;
    }
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
           this.transactionManager = transactionManager;
    }
    public DataSource getDataSource() {
          return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
         this.dataSource = dataSource;
    }
    public void insertUser() {
         TransactionTemplate tt =new TransactionTemplate(getTransactionManager());
         tt.execute(new TransactionCallback() {
                   public Object doInTransaction(TransactionStatus status) {
                        JdbcTemplate jt = new JdbcTemplate(getDataSource());
                        jt.update("insert into users (username) values ('xiaxin');");
                        jt.update("insert into users (id,username) values(2,'erica');");
                        return null;
                      }
                    });
    }
    }



(3)其中我的问题是针对以上的java源代码,它如何体现了类似手动写代码时候的事务原理?
首先设置手动提交事务,然后提交?
产生这个问题的疑惑是:以下这段代码,Spring中的JdbcTemplate操作采用的是JDBC默认的AutoCommit模式,也就是说我们还
无法保证数据操作的原子性(要么全部生效,要么全部无效),
JdbcTemplate jt = new JdbcTemplate(getDataSource());
jt.update("insert into users (username) values ('xiaxin');");//执行这sql语句后,我认为应该马上提交,何来的事务?
jt.update("insert into users (id,username) values(2,'erica');");//执行这sql语句后,我认为应该马上提交,何来的事务?
return null;
我的问题在于:::::::::
虽然,我举个例子(UserDAO类中,它已经获得一个transactionManager,而且,这个transactionManager已经获得了一个dataSource(不知道这样做有什么用?),好像这个transactionManager在UserDAO的事务中不起什么作用,反而起作用的JdbcTemplate 又体现不了手动写事务时的事务原理?为什么,请大家解释一下吧

我们都知道事务管理是通过获得方法里的数据库连接实现的,Spring怎么就这么聪明,知道要用哪个连接来管理事务,其中的奥迷不是这么简单哦。。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值