JAVA框架03 -- Spring

概念相关

spring 是什么

  • Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control: 反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术

spring 的优势

  • 方便解耦,简化开发

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

  • AOP 编程的支持
  • 声明式事务的支持
  • 方便程序的测试
  • 方便集成各种优秀框架 Spring 可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz 等)的直接支持。
  • 降低 JavaEE API 的使用难度 Spring 对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的使用难度大为降低。

耦合

概念

  • 耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调 用模块的方式以及通过界面传送数据的多少

包含

类之间的耦合
  • 使用反射来创建对象,避免使用new 关键字
  • 通过读取配置文件来获取要创建的对象全限定类名
    public static void main(String[] args) throws Exception {
// DriverManager.registerDriver(new com.mysql.jdbc.Driver()); 耦合性高
Class.forName("com.mysql.jdbc.Driver"); //全限定类名写死
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/easy","root", "123456");
final PreparedStatement pstm = connection.prepareStatement("select * from account");
final ResultSet resultSet = pstm.executeQuery();
while (resultSet.next()){
System.out.println(resultSet.getString("name"));
}

resultSet.close();
pstm.close();
connection.close();
}
方法间的耦合 需要做到:编译器不依赖,执行期再依赖

创建Bean对象

  • Bean:在计算机英语中,为可重用组件的含义

  • JavaBean: Javabean并不为实体类,实体类只是其一部分,其是用Java语言编写的可重用组件

  • 它是创建其service 和 dao 对象的

    1. 一个配置文件进行配置service和dao

      配置内容:唯一标识=全限定类名(key=value)

      配置文件:xml或者properties

    2. 通过读取配置文件中的配置内容,反射创建对象

工厂模式解藕合

  • 其产生的为多例,每次使用都会新创建一个实例,效率会低于单例
## bean.properties

accountService=com.jwang.service.impl.AccountServiceImpl
accountDao=com.jwang.dao.impl.AccountDaoImpl

## BeanFactory
public class BeanFactory {
//定义一个properties对象
private static Properties props;

/**
* 使用静态代码块为其赋值
*/
static {
try {
//实列化对象
props = new Properties();
//读取流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化properties失败");
}
}

/**
* 根据bean的名称获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
bean = Class.forName(beanPath).newInstance();
System.out.println(bean);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return bean;
}
}


/**
* 账户业务层实现类
*/
public class AccountServiceImpl implements IAccountService {

// private IAccountDao accountDao = new AccountDaoImpl();
private IAccountDao accountDao = (IAccountDao)BeanFactory.getBean("accountDao");
private int i = 1;
public void saveAccount() {
accountDao.saveAcccount();
System.out.println(i);
i++;
}
}


/**
* 账户业务层实现类
*/
/**
* 模拟表现层servlet 调用业务层
*/
public class Client {
public static void main(String[] args) {

for (int i = 0; i < 3; i++) {
IAccountService as = (IAccountService)BeanFactory.getBean("accountService");
as.saveAccount();
}
}
}

>>>
com.jwang.dao.impl.AccountDaoImpl@1218025c
com.jwang.service.impl.AccountServiceImpl@816f27d
保存账户完成
1
com.jwang.dao.impl.AccountDaoImpl@87aac27
com.jwang.service.impl.AccountServiceImpl@3e3abc88
保存账户完成
1
com.jwang.dao.impl.AccountDaoImpl@6ce253f1
com.jwang.service.impl.AccountServiceImpl@53d8d10a
保存账户完成
1
  • 使用单例,使其只创建一次对象 让其保存在一个容器中 后期就不用反复创建
  • 使用单例,在业务层和持久层没有值的改变,其效果更好,不会出现线程安全
public class BeanFactory {
//定义一个properties对象
private static Properties props;
//定义一个Map用于存放需要创建的对象--容器
private static Map<String, Object> beans;

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


/**
* 根据bean的名称获取bean对象 --单例
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
>>>
com.jwang.service.impl.AccountServiceImpl@1218025c
1
保存账户完成
com.jwang.service.impl.AccountServiceImpl@1218025c
1
保存账户完成
com.jwang.service.impl.AccountServiceImpl@1218025c
1
保存账户完成

Spring的IoC

概念

<?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">
<!-- 把对象的创建交给Spring管理 唯一标识和全限定类名-->
<bean id="accountService" class="com.jwang.service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="com.jwang.dao.impl.AccountDaoImpl"></bean>
</beans>
public class Client {
/**
* 获取soring的Ioc核心容器,并根据id获取对象
* @param args
*/
public static void main(String[] args){
//获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//根据id获取bean对象
IAccountService as = (IAccountService) ac.getBean("accountService");
IAccountDao ad = ac.getBean("accountDao", IAccountDao.class);

System.out.println(as);
System.out.println(ad);
}

ApplicationContext三个常用实现类

  • ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下
  • FileSystemXmlApplicationContext:加载磁盘任意路径下的配置文件(必须有访问权限)绝对路径
  • AnnotationConfigApplicationContext:读取注解创建容器

核心容器的两个接口

ApplicationContext:一般使用该接口,可以判断是否是单例
  • 构建核心容器时,创建对象采取的是立即加载的方式,只要一读取配置文件马上就创建配置文件中的对象(单例对象)
BeanFactory:
  • 创建对象采取延迟加载的方式,在根据id获取对象时才创建对象(多例对象使用)

spring对bean的管理细节

创建bean的三种方法

public class Client {
/**
* 获取soring的Ioc核心容器,并根据id获取对象
* @param args
*/
public static void main(String[] args){
//获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//根据id获取bean对象
IAccountService as = (IAccountService) ac.getBean("accountService");
System.out.println(as);
}
}
  • 使用默认构造函数创建:在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时,采用默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建
<bean id="accountService" class="com.jwang.service.impl.AccountServiceImpl"></bean>
  • 使用普通工厂中的方法创建对象 (使用某个类中的方法创建对象,并存入Spring容器)

<bean id="instanceFactory" class="com.jwang.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>

/**
* 模拟一个工厂类(jar包,无法修改源码)
*/
public class InstanceFactory {
public IAccountService getAccountService(){
return new AccountServiceImpl();
}
}
  • 使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入Spring容器)
<bean id="accountService" class="com.jwang.factory.StaticFactory" factory-method="getAccountService"></bean>

bean对象的作用范围

  • bean标签的scope属性:用于指定bean的作用范围
  • 取值:
    • singleton:单例的 - 默认值)
    • prototype:多例
    • request:作用于web的请求范围:
    • session:作用于web应用的会话范围
    • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,就是session
  • 集群:一个应用在多个服务器上部署则有多个ip地址,其访问网址的请求通过负载均衡访问,其产生的session在多个服务器上的公用session就是global-session

bean对象的生命周期

  • 单例对象:当容器创建时对象就产生,直到容器销毁,对象才销毁,即单例对象的生命周期与容器相同
  • 多例对象:使用对象时Spring框架为我们创建,只要使用过程中就一直存在,当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收机制回收
  • bean标签生命周期属性:
    • init-method=”初始化调用的该类中方法名”
    • destroy-method=”对象销毁调用的方法名”

Spring的依赖注入DI

依赖注入

  • Dependency Injection:依赖关系的维护就为依赖注入

  • IOC的作用:降低程序间的耦合(依赖关系)

  • 依赖关系的管理:交给Spring维护,当前类中需要用到其他类的对象,由Spring提供,只需要在配置文件中说明

  • 分类:

    • 基本类型和String
    • 其他bean类型(在配置文件中或者注解配置过的bean)
    • 复杂类型/集合类型
  • 注入方法(如果经常变化的数据,并不适用注入方式):

    • 使用构造函数提供
    • 使用set方法提供
    • 使用注解

构造函数注入(不推荐)

  • 使用的标签:constructor-arg

  • 出现位置:bean标签内部

  • 标签属性:

    • type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
    • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置从0开始
    • name:用于指定给构造函数中指定名称的参数赋值(常用)
    • value:用于提供基本类型和string类型的数据
    • ref:用于指定其他的bean类型的类型数据,它指在spring的Ioc核心容器中出现的bean对象,比如配置的Data类
  • 优势:在获取bean对象时,注入数据是必须的,否则对象创建失败

  • 弊端:改变类(bean对象)的实例化方式,在创建对象时,如果用不到其中某些参数,也必须提供

## 模拟工厂类
public class AccountServiceImpl implements IAccountService {

//如果经常变化的数据,并不适用注入方式
private String name;
private Integer age;
private Date birthday;

public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}

public void saveAccount() {
System.out.println("accountService ...."+name+","+age+","+birthday);
}
}

## 模拟使用

public static void main(String[] args){
//获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//根据id获取bean对象
IAccountService as = (IAccountService) ac.getBean("accountService");
as.saveAccount();
}

## bean依赖注入,name的属性顺序需要一致

<bean id="accountService" class="com.jwang.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="ss"></constructor-arg>
<constructor-arg name="age" value="12"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<!-- 配置日期对象-->
<bean id="now" class="java.util.Date"></bean>

set方法注入(常用)

  • 使用的标签:property

  • 出现位置:bean标签内部

  • 标签属性:

    • name:用于指定注入时所调用的set方法名称(setUserName – userName)
    • value:用于提供基本类型和string类型的数据
    • ref:用于指定其他的bean类型的类型数据,它指在spring的Ioc核心容器中出现的bean对象,比如配置的Data类
  • 优势:创建对象时没有明确的限制,可以直接使用默认构造函数

  • 弊端:如果某个成员必须有值,则获取对象是有可能set方法没有执行

<!-- 配置日期对象-->
<bean id="now" class="java.util.Date"></bean>

<bean id="accountService2" class="com.jwang.service.impl.AccountServiceImpl2">
<property name="userName" value="tesa"></property>
<property name="age" value="23"></property>
<property name="birthday" ref="now"></property>
</bean>

复杂类型注入/集合类型植入

  • 用于给list结构集合注入的标签:list array set
  • 用于给Map结构集合注入的标签:props map
<bean id="accountService3" class="com.jwang.service.impl.AccountServiceImpl3">
<property name="mystrs">
<array>
<value>aaa</value>
<value>bbb</value>
</array>
</property>
<property name="myMap">
<map>
<entry key="as" value="asa"></entry>
<entry key="ad"><value>asa</value></entry>
</map>
</property>
</bean>

基于注解的ioc以及ioc的使用

spring中ioc的常用注解

<?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.xsd">

<!--告知Spring在创建容器时,需要扫描的包,配置所需要的标签不在bean中,而是一个名称为context名称空间和约束中 -->
<context:component-scan base-package="com.jwang"></context:component-scan>
用于创建对象的注解
  • 和XML配置文件中编写一个bean标签实现的功能一样
  • @Component:将当前对象存入spring容器中
    • value:用于指定bean的id,不写时,默认值为类名,首字母变小写
  • 与Component的使用一样,但提供了明确的三层使用的注解,更加清晰(可以混用,都继承Component)
    • @Controller:表现层
    • @Service:业务层
    • @Repository:持久层
用于注入数据的
  • 在bean标签中写一个property标签作用一样
  • @Autowired:自动按照类型注入,只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功;若ioc容器中没有bean类型和要注入的变量类型匹配,则报错;若有多个匹配时,
    • 位置:成员变量和方法上等,在使用注解注入时,set方法就不是必须的

  • Qualifier:在按照类中注入的基础上再按照名称注入,在给类成员注入时不能单独使用,但是在给方法参数注入时可以
    • value:用于指定注入bean的id
  • Resource:直接按照bean的id注入,可以独立使用
    • name:用于指定bean的id

以上注解都只能注入其他bean类型的数据,而基本类型和String类型无法使用,另外,集合数据类型的注入只能使用xml实现

  • @Value:用于注入基本类型和String类型的数据
    • value:用于指定数据的值,它可以使用Spring中的SqEl(也就是spring的el表达式)
    • sqel表达式:${表达式}
用于改变作用范围的
  • 在bean标签中使用scope属性实现功能一样
  • @Scope:用于指定bean的作用范围
    • value:指定范围的取值:singleton/propertype
用于改变生命周期
  • 与bean标签的init-method以及destroy-method作用一样
  • @PreDestory:用于指定销毁方法
  • @PostConstruct:用于指定初始化方法
<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.xsd">

<!--告知spring在创建容器时要扫描的包 -->
<context:component-scan base-package="com.jwang"></context:component-scan>
<!--配置queryRunner对象(单例-多例) -->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源(构造函数)-->
<constructor-arg name="ds" ref="dataSources"></constructor-arg>
</bean>
<!--配置bean对象 -->
<bean id="dataSources" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/easy"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
</beans>
其他注解
  • Configuration: 指定当前类是一个配置类,当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写,只要不是其参数,就需要加上注解

  • ConponentScan:用于通过注解指定spring在创建容器时需要扫描的包

    • value:它和basePackages一样,都是用于指定创建容器时要扫描的包,使用此注解等于在xml中配置了<context:component-scan base-package="com.jwang"></context:component-scan>
  • Bean:用于把当前方法的返回值作为bean对象存入spring的ioc容器中

    • name:用于指定bean的id 不写,默认值为当前方法的名称
    • 使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象
  • Import:用于导入其他配置类

    • value:用于指定其他配置类的字节码,当使用该import注解后,有import注解的类就是主配置(父配置类) 导入的都是子配置类(子配置可以不写configuration和conponentscan)
  • PropertySource:用于指定properties文件的位置

    • value:指定文件的名称和路径
      • classpath关键字:表示类路径下
  • Qualifier:指定用哪一个id的bean, 可以写在对象前

@Configuration
@ComponentScan(basePackages = {"com.jwang"})
public class SpringConfiguration {

/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean
public QueryRunner creatQueryRunner(@Qualifier("id1") DataSource dataSource){
return new QueryRunner(dataSource);
}

/**
* 创建数据源对象
* @return
*/

@Bean(name="id1")
public DataSource creatDataSource() {
try {

ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/easy");
ds.setUser("root");
ds.setPassword("123456");
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

public class AccountServiceTest {

@Test
public void testFindAll(){
// final ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
final IAccountService as = ac.getBean("accountService", IAccountService.class);
List<Account> accounts = as.findAllAccount();
for (Account account : accounts) {
System.out.println(account);
}
}
}
Spring整合junit
  • 应用程序的入口:main方法

  • junit单元测试中,没有main方法也能执行; junit集成了一个main方法 ,该方法就会判断当前测试类中哪些方法有 @Test注解 junit就让有Test注解的方法执行

  • junit不会管我们是否采用spring框架,在执行测试方法时,junit根本不知道我们是不是使用了spring框架,所以也就不会为我们读取配置文件/配置类创建spring核心容器

  • 当测试方法执行时,没有Ioc容器,就算写了Autowired注解,也无法实现注入

  • 解决:导入spring整合junit的jar(坐标)使用Junit提供的一个注解把原有main方法替换替换成spring提供的@Runwith ,告知spring运行期,spring和ioc创建是基于创建是基于xml还是注解,并说明位置@ContextConfiguration(location:指定xml文件位置,加classpath关键字,表示在类路径下/classes:指定注解类所在位置)

  • 当我们使用spring5.x版本时,要求junit的jar包必须是4.1.2及以上

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
// @ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

@Autowired
private IAccountService as = null;

@Test
public void testFindAll(){
List<Account> accounts = as.findAllAccount();
for (Account account : accounts) {
System.out.println(account);
}
}
}

Spring的Aop

AOP概念

  • AOP:全称是 Aspect Oriented Programming 即:面向切面编程
  • 简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的 基础上,对我们的已有方法进行增强。
  • 实现方式:动态代理技术
  • spring中的aop:配置的方式

动态代理技术

特点
  • 字节码随用随创建,随用随加载。
  • 它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。
  • 装饰者模式就是静态代理的一种体现。
基于接口的动态代理
  • 提供者:JDK 官方的 Proxy 类。 要求:被代理类最少实现一个接口。
  • 涉及的类:Proxy 提供者:JDK官方
  • 创建代理对象:使用Proxy类的newProxyInstance方法
  • 创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用
  • newProxyInstance方法参数
    • ClassLoader:类加载器 用于加载代理对象字节码的,和被代理对象使用相同的类加载器。固定写法
    • Class[]:字节码数组 用于代理对象和被代理对象有相同方法,固定写法
    • InvocationHandler:用于提供增强的代码 让我们写如何代理。一般写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的,此接口谁用谁写
package com.jwang.proxy;

/**
* 对生产厂家要求的接口
*/
public interface IProducer {

/**
* 销售
* @param money
*/
public void saleProduct(float money);

/**
* 售后
* @param money
*/
public void afterService(float money);
}
package com.jwang.proxy;

/**
* 一个生产者
*/
public class Producer implements IProducer{

/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}

/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
package com.jwang.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* 模拟一个消费者
*/
public class Client {

public static void main(String[] args) {
final Producer producer = new Producer();

IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;

//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
基于子类的动态代理
  • 导入依赖
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
  • 涉及的类:Enhancer
  • 提供者:第三方cglib库
  • 如何创建代理对象:
    • 使用Enhancer类中的create方法
  • 创建代理对象的要求:
    • 被代理类不能是最终类
  • create方法的参数:
    • Class:字节码. 它是用于指定被代理对象的字节码。
    • Callback:用于提供增强的代码 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写。 我们一般写的都是该接口的子接口实现类:MethodInterceptor
/**
* 一个生产者
*/
public class Producer {

/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}

/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
/**
* 模拟一个消费者
*/
public class Client {

public static void main(String[] args) {
final Producer producer = new Producer();

Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;

//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}

Spring 中的AOP

AOP 相关术语
  • Joinpoint(连接点 ):所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。业务层的方法
  • Pointcut(切入点 ):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 invoke中的被增强方法
  • Advice(通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
  • Introduction(引介 ):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。
  • Target(目标对象 ):代理的目标对象。被代理对象
  • Weaving(织入 ): 是指把增强应用到目标对象来创建新的代理对象的过程。spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
  • Proxy(代理) :一个类被 AOP 织入增强后,就产生一个结果代理类。 代理对象
  • Aspect(切面 ):是切入点和通知(引介)的结合。

spring 中的 AOP 要明确的

开发阶段(我们做 的)
  • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
  • 把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
  • 在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。
运行阶段(Spring 框架完成的)
  • Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对 象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
代理的选择
  • 在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

基于 XML 的 AOP 配置

  • 配置spring的ioc把Service对象配置进来
spring中基于xml的aop配置
  • 1.把通知bean也交给spring 来管理
  • 2.使用aop:config标签来表明开始aop配置
  • 3.使用aop:aspect标签表明开始配置切面
    • id: 给切面一个唯一标志
    • ref: 指定通知类bean的ID
  • 4.在aop:aspect标签内部使用对应标签来配置通知类型(现在的例子是让pringLog方法在切入点方法执行前,所以是前置通知)
    • aop:before:表示前置通知(在切入点方法执行之前) /aop:after-returning: 后置通知 (在切入点方法正常执行之后)/aop:after-throwing :异常通知(在切入点方法异常执行后)/ aop:after:最后通知(相当于finally)
    • method:用于指定Logger类中的哪个方法是前置通知
    • pointcut:用于指定切入点表达式,其含义指对业务层中哪一个方法增强

切入点表达式:
  • 关键字:execution(表达式)

  • 表达式:访问修饰符 返回值 包名.包名.方法名(参数列表)

  • 标准写法:execution(public void com.jwang.service.impl.AccountServiceImpl.saveAccount()

  • 访问修饰符可以省略

  • 返回值可以使用通配符,表示任意返回值

  • 包名可以使用通配符,表示任意包。但有几级包,就需要几个*.

  • 包名可以使用..表示当前包及其子包

  • 类名和方法名都可以使用* 来实现通配

  • 参数列表:可以直接写数据类型:基本数据类型直接写 int 引用类型写包名.类名的方法:java.lang.String 可以使用通配符表示任意类型(必须有参数) ..表示有无参数均可,有参数可以是任意类型

  • 全通配写法:* *..*.*(..)

  • 实际写法:切到业务层实现类下的所有方法: * com.jwang.service.impl..(..)

  • 配置切入点表达式 id属性用于指定表达式的唯一标志,expression属性用于指定表达式内容

此标签写在aop:aspect标签内部只能当前切面可用

此标签写在外面,此时所有切面都可以使用,其必须在其之前(有顺序要求)

    <!--配置aop -->
<aop:config>
<aop:pointcut id="ptl" expression="execution(* com.jwang.service.impl.*.*(..))"/>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,建立通知方法和切入点方法的关联 -->
<aop:before method="printLog" pointcut-ref="ptl"></aop:before>
<!--配置切入点表达式 id属性用于指定表达式的唯一标志,expression属性用于指定表达式内容-->
<!-- <aop:pointcut id="ptl" expression="execution(* com.jwang.service.impl.*.*(..))"/>-->
</aop:aspect>
</aop: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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置spring的ioc把Service对象配置进来-->
<bean id="service" class="com.jwang.service.impl.AccountServiceImpl"></bean>

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

<!--配置aop -->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,建立通知方法和切入点方法的关联 在业务层service的saveAccount()方法前执行 printLog-->
<aop:before method="printLog" pointcut="execution(
public void com.jwang.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
配置环绕通知
  • 配置环绕通知后,切入点方法没有执行,通知方法执行的原因:动态代理中的环绕通知有明确的切入点方法调用,而我们的代码中没有
  • 解决:Spring框架为我们提供了一个接口,PreceedingJoinPoint。该接口的方法proceed()此方法相当于明确调用切入点方法。该接口作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用
  • Spring的环绕通知:是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
>>>bean.xml添加
<aop:around method="aroudPrintLog" pointcut-ref="ptl"/>

>>>环绕通知方法
public Object aroudPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行时需要的参数

System.out.println("Logger中的aroudprintLog开始记录日志--前置");

rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("Logger中的aroudprintLog开始记录日志--后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger中的aroudprintLog开始记录日志--异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger中的aroudprintLog开始记录日志--最终");
}
}

基于注解的AOP配置

  • Spring的其执行顺序有一定问题,但环绕通知没有问题(是自己写的)
    <!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.jwang"/>
<aop:aspectj-autoproxy/>

>>>impl
@Component("logger")
@Aspect //表示当前类是切面类
public class Logger {

/**
* 用于打印日志,计划让其在切入点方法执行前执行(也就是业务层方法执行后)
* 前置通知
*/

@Pointcut("execution(* com.jwang.service.impl.*.*(..))")
private void tt1(){}

@Before("tt1()")
public void printLog(){
System.out.println("Logger中的printLog开始记录日志");
}

Spring中的JdbcTemplate

概念

  • 它是 spring 框架中提供的一个对象,是对原始 Jdbc API 对象的简单封装。spring 框架为我们提供了很多 的操作模板类。
  • 操作关系型数据的: JdbcTemplate,HibernateTemplate
  • 操作nosql 数据库的: RedisTemplate
  • 操作消息队列的: JmsTemplate

JdbcTemplate 对象的创建

  • 使用前提:包在:spring-jdbc-5.0.2.RELEASE.jar 中,我们在导包的时候,除了要导入这个 jar 包
    外,还需要导入一个 spring-tx-5.0.2.RELEASE.jar(它是和事务相关的)。
public class JdbcTemplateDemo1 {
public static void main(String[] args) {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/easy");
ds.setUsername("root");
ds.setPassword("123456");


JdbcTemplate jt = new JdbcTemplate();
jt.setDataSource(ds);
jt.execute("insert into account(name, money)values('ccc', 1000)");
}
}
  • 使用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:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/easy"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>
public class JdbcTemplateDemo2 {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate jt = ac.getBean("jdbcTemplate", JdbcTemplate.class);
jt.execute("insert into account(name, money)values('eee', 1000)");
}
}
  • JdbcTemplate 的crud操作
public class JdbcTemplateDemo2 {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate jt = ac.getBean("jdbcTemplate", JdbcTemplate.class);

//crud操作
//保存
jt.update("insert into account(name, money)values(?,?)", "eed",3333f);
//更新
jt.update("update account set name = ?, money=? where id = ?", "eee",122f, 3);
//删除
jt.update("delete from account where id = ?", 6);

//查询
List<Account> accounts = jt.query("select * from account where money > ?",
new BeanPropertyRowMapper<Account>(Account.class), 999f);

for (Account account : accounts) {
System.out.println(account);
}

//查询所有
List<Account> accounts = jt.query("select * from account",new BeanPropertyRowMapper<Account>(Account.class));
for (Account account : accounts) {
System.out.println(account);
}

final Integer count = jt.queryForObject("select count(*) from account where money >= ?", Integer.class, 1000f);
System.out.println(count);
}
}

spring中的事务控制的API

  • 第一:JavaEE 体系进行分层开发,事务处理位于业务层,Spring 提供了分层设计业务层的事务处理解决方 案。
  • 第二:spring 框架为我们提供了一组事务控制的接口。具体在后面的第二小节介绍。这组接口是在 spring-tx-5.0.2.RELEASE.jar 中。
  • 第三:spring 的事务控制都是基于 AOP 的,它既可以使用编程的方式实现,也可以使用配置的方式实现。我 们学习的重点是使用配置的方式实现。
PlatformTransactionManager
  • 此接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法 在开发中都是使用它的实现类
    • org.springframework.jdbc.datasource.DataSourceTransactionManager 使用 Spring JDBC 或 iBatis 进行持久化数据时使用
    • org.springframework.orm.hibernate5.HibernateTransactionManager 使用 Hibernate 版本进行持久化数据时使用
TransactionStatus

基于 XML 的声明式事务控制(配置方式)

步骤

环境搭建
  • 第一步:拷贝必要的 jar 包到工程的 lib 目录
  • 第二步:创建 spring 的配置文件并导入约束
此处需要导入 aop 和 tx 两个名称空间
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"> </beans>
  • 第三步:准备数据库表和实体类
  • 第四步:编写业务层接口和实现类
  • 第五步:编写 Dao 接口和实现类
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
@Override
public Account findAccountById(Integer id) {
List<Account> list = getJdbcTemplate().query("select * from account where id = ? ",
new AccountRowMapper(),id);
return list.isEmpty()?null:list.get(0);
}

@Override
public Account findAccountByName(String name) {
List<Account> list = getJdbcTemplate().query("select * from account where name = ? ",
new AccountRowMapper(),name);
if(list.isEmpty()){
return null;
} if(list.size()>1){
throw new RuntimeException("结果集不唯一,不是只有一个账户对象");
}
return list.get(0); }

@Override
public void updateAccount(Account account) {
getJdbcTemplate().update("update account set money = ? where id = ? ",
account.getMoney(),account.getId());
}
}


/**
* 账户的封装类 RowMapper 的实现类
*/
public class AccountRowMapper implements RowMapper<Account>{
@Override
public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
Account account = new Account();
account.setId(rs.getInt("id"));
account.setName(rs.getString("name"));
account.setMoney(rs.getFloat("money"));
return account;
}
}
  • 第六步:在配置文件中配置业务层和持久层

<!-- 配置 service -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 配置 dao -->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!-- 注入 dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///spring_day04"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>
</bean>
基于xml的配置事务步骤
  • 第一步:配置事务管理器
<!-- 配置一个事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入 DataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
  • 第二步:配置事务的通知引用事务管理器
<!-- 事务的配置 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager"> </tx:advice>
  • 第三步:配置事务的属性
<!--在 tx:advice 标签内部 配置事务的属性 --> 

<tx:attributes>
<!-- 指定方法名称:是业务核心方法 read-only:是否是只读事务。默认 false,不只读。
isolation:指定事务的隔离级别。默认值是使用数据库的默认隔离级别。 propagation:指定事务的传播行为。
timeout:指定超时时间。默认值为:-1。永不超时。
rollback-for:用于指定一 个异常,当 执行产生该 异常时,事 务回滚。
产 生其他异常 ,事务不回 滚。没有默认值,任何异常都回滚。
no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,
产生其他异常时,事务回滚。没有默认值,任何异常都回滚。
-->
<tx:method name="*" read-only="false" propagation="REQUIRED"/>
<tx:method name="find*" read-only="true" propagation="SUPPORTS"/> </tx:attributes>
  • 第四步:配置 AOP 切入点表达式
<!-- 配置 aop -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/>
</aop:config>
  • 第五步:配置切入点表达式和事务通知的对应关系
<!-- 在 aop:config 标签内部:建立事务的通知和切入点表达式的关系 --> 
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>

基于注解的事务配置

步骤

  • 第一步:拷贝必备的 jar 包到工程的 lib 目录
  • 第二步:创建 spring 的配置文件导入约束并配置扫描的包
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置 spring 创建容器时要扫描的包 -->
<context:component-scan base-package="com.jwang"></context:component-scan>
<!-- 配置 JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/easy"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
  • 第三步:创建数据库表和实体类(与xml配置相同)
  • 第四步:创建业务层接口和实现类并使用注解让 spring 管理
/**
* 账户的业务层实现类 */
@Service("accountService") public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
//其余代码和基于 XML 的配置相同
}
  • 第五步:创建 Dao 接口和实现类并使用注解让 spring 管理
/**
* 账户的持久层实现类 */
@Repository("accountDao") public class AccountDaoImpl implements IAccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//其余代码和基于 XML 的配置相同
}

事务纯注解配置

  • 第一步:配置事务管理器并注入数据源
<!-- 配置事务管理器 -->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property>

</bean>
  • 第二步:在业务层使用@Transactional 注解
@Service("accountService") 
@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;

@Override
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}

@Override
@Transactional(readOnly=false,propagation=Propagation.REQUIRED)
public void transfer(String sourceName, String targeName, Float money) {
//1.根据名称查询两个账户
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targeName);
//2.修改两个账户的金额 source.setMoney(source.getMoney()-money);//转出账户减钱
target.setMoney(target.getMoney()+money);//转入账户加钱
// 3.更新两个账户
accountDao.updateAccount(source); //int i=1/0; accountDao.updateAccount(target);
}
}

该注解的属性和 xml 中的属性含义一致。
该注解可以出现在接口上,类上和方法上。
出现接口上,表示该接口的所有实现类都有事务支持。
出现在类上,表示类中所有方法有事务支持 出现在方法上,表示方法有事务支持。
以上三个位置的优先级:方法>类>接口
  • 第三步:在配置文件中开启 spring 对注解事务的支持
 <!-- 开启 spring 对注解事务的支持 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
  • 不使用 xml 的配置方式
@Configuration
@EnableTransactionManagement
public class SpringTxConfiguration {
//里面配置数据源,配置 JdbcTemplate,配置事务管理器。详细内容在下面
}
配置jdbc config
  • SpringConfiguration
/**
* spring的配置类,相当于bean.xml
*/
@Configuration
@ComponentScan("com.itheima")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("jdbcConfig.properties")
@EnableTransactionManagement
public class SpringConfiguration {
}
  • JdbcConfig

/**
* 和连接数据库相关的配置类
*/
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;

/**
* 创建JdbcTemplate
* @param dataSource
* @return
*/
@Bean(name="jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}

/**
* 创建数据源对象
* @return
*/
@Bean(name="dataSource")
public DataSource createDataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}
  • TransactionConfig
/**
* 和事务相关的配置类
*/
public class TransactionConfig {

/**
* 用于创建事务管理器对象
* @param dataSource
* @return
*/
@Bean(name="transactionManager")
public PlatformTransactionManager createTransactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值