Spring AOP 开发实践

AOP的概述

1. 什么是AOP的技术?
	* 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
	* AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构
	* AOP最早由AOP联盟的组织提出的,制定了一套规范。Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范
	* 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
	* AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型
	* 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

2. AOP:面向切面编程.(思想---解决OOP遇到一些问题)
3. AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)

4. 可以在不修改源代码的前提下,对程序进行增强

Spring框架的AOP的底层实现

1. Srping框架的AOP技术底层也是采用的代理技术,代理的方式提供了两种
	(1) 基于JDK的动态代理
		* 必须是面向接口的,只有实现了具体接口的类才能生成代理对象
	
	(2) 基于CGLIB动态代理
		* 对于没有实现了接口的类,也可以产生代理,产生这个类的子类的方式

2. Spring的传统AOP中根据类是否实现接口,来采用不同的代理方式
	(1) 如果实现类接口,使用JDK动态代理完成AOP
	(2) 如果没有实现接口,采用CGLIB动态代理完成AOP

JDK的动态代理

这里使用动态代理工厂演示

1. 创建前置增加接口
public interface AdviseBefore {
	public void before();
}
2. 创建后置增加接口
public interface AdviseAfter {
	public void after();
}
3. 创建用户服务接口
public interface UserService {
	boolean save();
}
4. 创建用户服务实现类
public class UserServiceImpl implements UserService {
	@Override
	public boolean save() {
		System.out.println("向数据库保存数据...");
		return true;
	}
}
5. 创建动态代理工厂
/**
 * 动态代理工厂
 */
public class ProxyFactory {
	// 要增强的目标对象
	private Object target;
	// 前置增强
	private AdviseBefore adviseBefore;
	// 后置增强
	private AdviseAfter adviseAfter;
	
	public ProxyFactory() {
		
	}

	public ProxyFactory(Object target, AdviseBefore adviseBefore, AdviseAfter adviseAfter) {
		this.target = target;
		this.adviseBefore = adviseBefore;
		this.adviseAfter = adviseAfter;
	}

	public Object createProxy() {
		// 获取类加载器
		ClassLoader loader = this.getClass().getClassLoader();
		// 获取目标对象的所有接口
		Class<?>[] interfaces = target.getClass().getInterfaces();
		// 构造InvocationHandler的实现类,该参数用于创建代理对象
		InvocationHandler hander = new InvocationHandler() {
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				// 调用目标方法前执行前置增强
				adviseBefore.before();
				// 调用目标方法
				Object result = method.invoke(target, args);
				// 调用目标方法后执行后置增强
				adviseAfter.after();
				return result;
			}
		};
		
		// 创建目标对象的代理对象
		Object proxy = Proxy.newProxyInstance(loader, interfaces, hander);
		return proxy;
	}
	
	/**
	 * 传入目标对象
	 * @param target 目标对象
	 */
	public void setTarget(Object target) {
		this.target = target;
	}

	/**
	 * 传入前置增强接口的实现类
	 * @param adviseBefore 前置增强接口的实现类
	 */
	public void setAdviseBefore(AdviseBefore adviseBefore) {
		this.adviseBefore = adviseBefore;
	}

	/**
	 * 传入后置增强接口的实现类
	 * @param adviseAfter 后置增强接口实现类
	 */
	public void setAdviseAfter(AdviseAfter adviseAfter) {
		this.adviseAfter = adviseAfter;
	}
}
6. 测试调用
	@Test
	public void testUserServiceProxy() {
		// 1. 获取目标对象
		UserService target = new UserServiceImpl();
		// 2. 前置增强接口实现
		AdviseBefore adviseBefore = new AdviseBefore() {
			public void before() {
				System.out.println("startTransaction!");
			}
		};
		// 3. 后置增强接口实现
		AdviseAfter adviseAfter = new AdviseAfter() {
			public void after() {
				System.out.println("commit!");
			}
		};
		
		// 4. 设置代理工厂
		ProxyFactory proxyFactory = new ProxyFactory(target, adviseBefore, adviseAfter);
//		proxyFactory.setAdviseBefore(adviseBefore);
//		proxyFactory.setTarget(target);
//		proxyFactory.setAdviseAfter(adviseAfter);  
		
		// 5. 获取目标代理
		UserService targetProxy = (UserService) proxyFactory.createProxy();
		
		// 6. 执行目标对象的save方法
		targetProxy.save();
	}
7.运行结果
startTransaction!
向数据库保存数据...
commit!

CGLIB动态代理

切面接口和上面的JDK代理定义一样,只是在UserviceImpl的save()上添加注解@MyTransaction ,然后在写一个CGLIB的动态代理工厂类演示
1. 添加自定义注解,目的是为了在执行代理方法时,用于判断是否执行切面aspect的增强方法
/**
 * 自定义事务注解
 */
@Target(value={ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTransaction {

}
2. 在实现类的方法上加注解@MyTransaction 
public class UserServiceImpl{
	@MyTransaction // 表示事务的自定义注解
	public boolean save() {
		System.out.println("向数据库保存数据...");
		return true;
	}
	
	public boolean update() {
		System.out.println("更新数据...");
		return true;
	}
}
3. CGLIB简单的泛型动态代理实现
/**
 * CGLIB动态代理工厂
 */
public class CGLibProxyFactory<T> {
	// 目标类的Class对象
	private Class<T> targetClazz;
	
	// 前置增强
	private AdviseBefore adviseBefore;
	
	// 后置增强
	private AdviseAfter adviseAfter;
	
	public CGLibProxyFactory(Class<T> targetClazz, AdviseBefore adviseBefore, AdviseAfter adviseAfter) {
		this.targetClazz = targetClazz;
		this.adviseBefore = adviseBefore;
		this.adviseAfter = adviseAfter;
	}

	public T createProxy() {
		// 创建CGLIB核心的类
		Enhancer enhancer = new Enhancer();
		// 设置父类
		enhancer.setSuperclass(targetClazz);
		// 设置回调函数
		enhancer.setCallback(new MethodInterceptor() {
			@Override
			public Object intercept(Object obj, Method method, Object[] args, 
									MethodProxy methodProxy) throws Throwable {
				// 获取自定义事务注解
				MyTransaction transaction = method.getAnnotation(MyTransaction.class);
				// 能获取到该注解说明要执行增强
				if (transaction != null) {
					adviseBefore.before();
				}
				// 执行代理方法
				Object object =  methodProxy.invokeSuper(obj, args);
				if (transaction != null) {
					adviseAfter.after();
				}
				return object;
			}
		});
		// 返回代理对象
		return (T) enhancer.create();
	}
}
4. 运行测试
	@Test
	public void run() {
		// 2. 前置增强接口实现
		AdviseBefore adviseBefore = new AdviseBefore() {
			public void before() {
				System.out.println("start transaction!");
			}
		};
		// 3. 后置增强接口实现
		AdviseAfter adviseAfter = new AdviseAfter() {
			public void after() {
				System.out.println("commit!");
			}
		};
		// 传入UserServiceImpl.class及增强
		CGLibProxyFactory<UserServiceImpl> proxyFactory = new CGLibProxyFactory<>(UserServiceImpl.class, adviseBefore, adviseAfter);
		UserServiceImpl userServiceImpl = proxyFactory.createProxy();
		userServiceImpl.save();
		System.out.println("-------------------------");
		userServiceImpl.update();
	}
运行结果
start transaction!
向数据库保存数据...
commit!
-------------------------
更新数据...

技术分析之AOP的相关术语

1. Joinpoint(连接点)	-- 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为Spring只支持方法类型的连接点
2. Pointcut(切入点)	-- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
3. Advice(通知/增强)	-- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
4. Introduction(引介)-- 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field
5. Target(目标对象)	-- 代理的目标对象
6. Weaving(织入)		-- 是指把增强应用到目标对象来创建新的代理对象的过程
7. Proxy(代理)		-- 一个类被AOP织入增强后,就产生一个结果代理类
8. Aspect(切面)		-- 是切入点和通知的结合,自己来编写和配置的

结合下图可更好的理解术语
在这里插入图片描述


Spring XML配置AOP开发

1. 步骤一:引入具体的开发的jar包
	* 先引入Spring框架开发的基本开发包
	* 再引入Spring框架的AOP的开发包
		* spring的传统AOP的开发的包
			* spring-aop-4.2.4.RELEASE.jar
			* com.springsource.org.aopalliance-1.0.0.jar
		
		* aspectJ的开发包
			* com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
			* spring-aspects-4.2.4.RELEASE.jar

2. 步骤二:创建Spring的配置文件,引入具体的AOP的schema约束
	<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">
	</beans>

3. 步骤三:创建包结构,编写具体的接口和实现类
	* com.yp.demo
		* CustomerDao			-- 接口
		* CustomerDaoImpl		-- 实现类
public interface CustomerDao {
	public void save();
	public void update();
	public void delete();
}
public class CustomerDaoImpl implements CustomerDao {
	@Override
	public void save() {
		//int a = 10/0;
		System.out.println("保存客户...");
	}

	@Override
	public void update() {
		System.out.println("更新客户...");
	}

	@Override
	public void delete() {
		System.out.println("删除客户...");
	}
}
4. 步骤五:定义切面类
public class CustomerAspect {
	public void log() {
		System.out.println("记录日志...");
	}
	
	public void around(ProceedingJoinPoint joinPoint) {
		System.out.println("前置增强...");
		try {
			joinPoint.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		System.out.println("后置增强...");
	}
}
5. 步骤四:将目标类配置到applicationContext.xml中
	<bean id="customerDao" class="com.yp.demo.CustomerDaoImpl"/>

6. 步骤六:将切面类配置到applicationContext.xml中
	<bean id="customerAspect " class="com.yp.demo.CustomerAspect "/>
<?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">
	<!-- 配置目标类 -->
	<bean id="customerDao" class="com.yp.demo.CustomerDaoImpl"/>
	
	<!-- 配置切面类 -->
	<bean id="customerAspect" class="com.yp.demo.CustomerAspect"/>
	
	<!-- 配置AOP -->
	<aop:config>
		<!-- 这里可以定义一个pointcut,这样就可以复用 -->
		<aop:pointcut expression="execution(* *..*.*DaoImpl.save*(..))" id="pointcut2"/>
		<!-- 引入切面类 -->
		<aop:aspect ref="customerAspect">
			<!--下面通过例子说明SpEL的写法 -->
			<!-- 定义advice类型: 切面类的方法和切入点的表达式 -->
			<!-- <aop:before method="log" pointcut="execution(public void com.yp.demo.CustomerDaoImpl.save(..))"/> -->
			
			<!-- public可以不写 
			<aop:before method="log" pointcut="execution(void com.yp.demo.CustomerDaoImpl.save(..))"/>-->
			
			<!-- 返回值可以使用*代替,但不能省略
			<aop:before method="log" pointcut="execution(* com.yp.demo.CustomerDaoImpl.save(..))"/>-->
			
			<!-- 包名可以使用*代替,但不能省略
			<aop:before method="log" pointcut="execution(* com.yp.*.CustomerDaoImpl.save(..))"/> -->
			
			<!-- 多段包名可以使用*..*代替,但不能省略
			<aop:before method="log" pointcut="execution(* *..*.CustomerDaoImpl.save(..))"/> -->
			
			<!-- 类的写法,使用*DaoImpl通配以DaoImpl结尾的类
			<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save(..))"/> -->
			
			<!-- 方法的写法,使用save*()通配以save开头的方法 ".."表示多个任意参数个数
			<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*(..))"/> -->
			
			<!-- 定义一个pointcut,这样就可以复用 -->
			<aop:pointcut expression="execution(* *..*.*DaoImpl.save*(..))" id="pointcut1"/>
			
			<!-- 引用定义的pointcut -->
			<aop:before method="log" pointcut-ref="pointcut1"/>			
			<!-- 无参 -->
			<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.delete*())"/>
			
			<!-- 参数"*"代表一个参数 -->
			<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.delete*(*))"/>
		</aop:aspect>
	</aop:config>
</beans>
7. 完成测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
//@ContextConfiguration("classpath:applicationContext1.xml")
public class Demo {	
	@Resource(name="customerDao")
	private CustomerDao customerDao;
	
	@Test
	public void run() {
		customerDao.save();
		System.out.println("-------------");
		customerDao.update();
		System.out.println("-------------");
		customerDao.delete();
	}	
}
运行结果
记录日志...
保存客户...
-------------
更新客户...
-------------
记录日志...
删除客户...

AOP的通知类型

1. 前置通知
	* 在目标类的方法执行之前执行。
	* 配置文件信息:<aop:before method="before" pointcut-ref="execution(* *..*.*.*(..))"/>
	* 应用:可以获取方法参数,并对方法的参数来做校验

2. 最终通知,相当于在finally块中执行
	* 在目标类的方法执行之后执行,如果程序出现了异常,最终通知也会执行。
	* 在配置文件中编写具体的配置:<aop:after method="after" pointcut-ref="execution(* *..*.*.*(..))"/>
	* 应用:例如像释放资源

3. 后置通知
	* 方法正常执行后的通知。		
	* 在配置文件中编写具体的配置:<aop:after-returning method="afterReturning" pointcut-ref="execution(* *..*.*.*(..))"/>
	* 应用:可以获取返回值,但不能修改返回值

4. 异常抛出通知
	* 在抛出异常后通知
	* 在配置文件中编写具体的配置:<aop:after-throwing method="afterThorwing" pointcut-ref="execution(* *..*.*.*(..))"/>	
	* 应用:包装异常的信息

5. 环绕通知
	* 方法的执行前后执行。
	* 在配置文件中编写具体的配置:<aop:around method="around" pointcut-ref="execution(* *..*.*.*(..))"/>
	* 要注意:目标的方法默认不执行,需要使用ProceedingJoinPoint对来让目标对象的方法执行,可以修改返回值。

将上面的类做修改,或重新建立工程
CustomerDao 接口

public interface CustomerDao {
	public void save(String msg);
	public Boolean update();
	public Boolean delete(Long id, String name);
	String query(String name);
}

CustomerDao的实现类CustomerDaoImpl

public class CustomerDaoImpl implements CustomerDao {
	@Override
	public void save(String msg) {
		System.out.println(msg);
	}

	@Override
	public Boolean update() {
		System.out.println("更新客户...");
		return true;
	}

	@Override
	public Boolean delete(Long id, String name) {
		System.out.println("删除客户...");
		return false;
	}
	
	@Override
	public String query(String name){
		int a = 10 /0;
		return "查询的结果";
	}
}

切面类CustomerAspect

public class CustomerAspect {
	public void log(JoinPoint joinPoint) {
		// 获取方法参数
		Object[] args = joinPoint.getArgs();
		System.out.println("args" + Arrays.toString(args));
		System.out.println("记录日志...");
	}
	
	public Object afterReturning(JoinPoint joinPoint, Object rvt) {
		// 获取方法参数
		Object[] args = joinPoint.getArgs();
		System.out.println("args: " + Arrays.toString(args));
		
		// 获取返回值
		System.out.println("return: " + rvt);
		rvt = false;
		System.out.println("编辑返回值...");
		
		return rvt;
	}
	
	public Object around(ProceedingJoinPoint joinPoint) {
		Object[] args = joinPoint.getArgs();
		System.out.println("args: " + Arrays.toString(args));
		System.out.println("前置增强...");
		try {
			Object obj = joinPoint.proceed();
			System.out.println("return: " + obj);
		} catch (Throwable e) {
			e.printStackTrace();
		}
		System.out.println("后置增强...");
		
		return true;
	}
	
	public Object afterThrowing(JoinPoint joinPoint, Exception ex) {
		// 获取方法参数
		Object[] args = joinPoint.getArgs();
		System.out.println("args: " + Arrays.toString(args));
		
		// 获取返回值
		System.out.println("exception message: " + ex.getMessage());
		
		return ex.getMessage();
	}
	
	public Object after(JoinPoint joinPoint, Boolean result) {
		// 获取方法参数
		Object[] args = joinPoint.getArgs();
		System.out.println("args: " + Arrays.toString(args));
		
		// 获取返回值
		System.out.println("return: " + result);
		System.out.println("编辑返回值...");
		
		return result;
	}
}

applicationContext1.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">
	<!-- 配置目标类 -->
	<bean id="customerDao" class="com.yp.demo.CustomerDaoImpl"/>
	
	<!-- 配置切面类 -->
	<bean id="customerAspect" class="com.yp.demo.CustomerAspect"/>
	
	<!-- 配置AOP -->
	<aop:config>
		<!-- 引入切面类 -->
		<aop:aspect ref="customerAspect">
			<!-- 前置通知 -->
			<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*(..))"/>
			
			<!-- 最终通知,有异常也执行
			<aop:after method="edit" pointcut="execution(* *..*.*DaoImpl.update*(..))"/> -->
			
			<!-- 后置通知,有异常不执行 -->
			<aop:after-returning method="afterReturning" pointcut="execution(* *..*.*DaoImpl.update*(..))" returning="rvt"/>
			
			<!-- 异常通知,当抛出异常时执行 -->
			<aop:after-throwing method="afterThrowing" pointcut="execution(* *..*.*DaoImpl.query*(..))" throwing="ex"/>
			
			<!-- 环绕通知,前后都执行 -->
			<aop:around method="around" pointcut="execution(* *..*.*DaoImpl.delete*(..))"/>
		</aop:aspect>
	</aop:config>
</beans>

测试代码:

@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration("classpath:applicationContext.xml")
@ContextConfiguration("classpath:applicationContext1.xml")
public class Demo {	
	@Resource(name="customerDao")
	private CustomerDao customerDao;
	
	@Test
	public void run() {
		customerDao.save("保存客户...");
		System.out.println("-------------");
		Boolean updateResult = customerDao.update();
		System.out.println("updateResult: " + updateResult);
		System.out.println("-------------");
		Boolean deleteResult = customerDao.delete(123L, "老王");
		System.out.println("deleteResult: " + deleteResult);
		System.out.println("-------------");
		String queryResult = customerDao.query("隔壁老王");
		System.out.println("queryResult: " + queryResult);
	}	
}

运行结果

args[保存客户...]
记录日志...
保存客户...
-------------
更新客户...
args: []
return: true
编辑返回值...
updateResult: true
-------------
args: [123, 老王]
前置增强...
删除客户...
return: false
后置增强...
deleteResult: true
-------------
args: [隔壁老王]
exception message: / by zero

Spring 注解配置AOP开发

1. 定义接口
public interface CustomerDao {
	public void save();
	public void update();
	public void delete();
}
2. 接口实现,这里都使用注解定义bean
@Repository(value="customerDao")
public class CustomerDaoImpl implements CustomerDao {
	@Override
	public void save() {
		System.out.println("保存客户...");
	}

	@Override
	public void update() {
		System.out.println("更新客户...");
	}

	@Override
	public void delete() {
		System.out.println("删除客户...");
	}
}
3. 定义并配置切面类
   (1. 通知类型
	* @Before				-- 前置通知
    * @AfterReturing		-- 后置通知
    * @Around				-- 环绕通知(目标对象方法默认不执行的,需要手动执行)
    * @After				-- 最终通知
    * @AfterThrowing		-- 异常抛出通知

   (2. 配置通用的切入点
	* 使用@Pointcut定义通用的切入点
@Component(value="myAspectAnno")
@Aspect
public class MyAspectAnno {
	
	//定义通知类型及切入点表达式
	@Before(value="execution(* com.*..*.*DaoImpl.delete*(..))")
	public void log() {
		System.out.println("记录日志...");
	}
	
	//环绕型通知
	@Around(value="execution(* com.*..*.*DaoImpl.save*(..))")
	public void around(ProceedingJoinPoint joinPoint) {
		System.out.println("around前...");
		try {
			joinPoint.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		System.out.println("around后...");
	}
	
	//使用通用切入点
	@After(value="MyAspectAnno.pointcut()")
	public void after() {
		System.out.println("after...");
	}
	
	//定义通用切入点,方法名字任意,和xml的<aop:pointcut />类似,然后使用pointcut-ref引用
	@Pointcut(value="execution(* com.*..*.*DaoImpl.update*(..))")
	public void pointcut() {}
}
4. applicationContext.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" 
    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/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
	<!-- 开启注解扫描 -->
	<context:component-scan base-package="com.yp"/>
	
	<!-- 开启自动代理 -->
	<aop:aspectj-autoproxy/>
</beans>
5. 单元测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext2.xml")
public class DemoAnno {

	@Resource(name="customerDao")
	private CustomerDao customerDao;
	
	@Test
	public void run() {
		customerDao.save();
		System.out.println("--------------------");
		customerDao.update();
		System.out.println("--------------------");
		customerDao.delete();
	}
}
运行结果
around前...
保存客户...
around后...
--------------------
更新客户...
after...
--------------------
记录日志...
删除客户...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值