Java--002 Spring 注解和AOP实现

目录

1. IOC注解

1.1 xml和注解的区别

1.1.1 XML配置

1.1.2 annotation配置

1.2  注解注入使用步骤

1.2.1  Autowired

1.2.2  Resource

1.3  注解实例化使用步骤

1.3.1  配置文件

1.3.2  业务类

1.3.3  测试

1.3.4  注解分类

1.3.5  注解区别

1.4  新注解

1.4.1、Configuration

1.4.2、ComponentScan

1.4.3、Bean

1.4.4 PropertySource

1.4.5 Import

2、AOP(面向切面编程)

2.1 为什么学习 AOP  

2.2 Spring 的 AOP 的由来

2.3 底层实现

2.4动态代理

2.5 AOP相关术语

2.6 注解方式

2.6.1 引入jar包

2.6.2 配置文件

2.6.3 AOP类

2.7 XML方式

2.7.1 引入jar包

2.6.3 AOP类


 

1. IOC注解

        Spring框架中有注解和XML两种配置方式,包括Spring中的IOC和AOP也一样,都有XML和注解两种方式两种方式各有千秋。

1.1 xml和注解的区别

1.1.1 XML配置

优点:

1.XMl配置方式进一步降低了耦合,使得应用更加容易扩展,即使对配置文件进一步修改也不需要工程进行修改和重新编译。

2.再处理打的业务量的时候,用XML配置应该更加好一些。因为XMl更加清晰的表明了各个对象之间的关系,各个业务类之间的调用。同时spring的次昂管配置也能一目了然。

缺点:

配置文件读取和解析需要花费一定的时间,配置文件过多的时候难以管理,无法对配置额正确性进行校验,增加了测试难度。

1.1.2 annotation配置

优点有:  

  1. 在class文件中,可以降低维护成本,annotation的配置机制很明显简单  
  2. 不需要第三方的解析工具,利用java反射技术就可以完成任务  
  3. 编译期可以验证正确性,查错变得容易
  4. 提高开发效率  

缺点有:  

  1. 如果需要对于annotation进行修改,那么要重新编译整个工程  
  2. 业务类之间的关系不如XML配置那样容易把握。  
  3. 如果在程序中annotation比较多,直接影响代码质量,对于代码的简洁度有一定的影响

Spring 的IOC 的XML方式我们已经掌握的差不多了,下面来学习学习注解的方式

1.2  注解注入使用步骤

1.2.1  Autowired

1.2.1.1  创建项目并导包

和昨天的步骤一样,不过需要引入一个新的jar包

spring-aop-4.2.1.RELEASE.jar

0

1.2.1.3  开启注解的支持

<?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">
	<!-- 引入注解约束 -->
	<!-- 使用注解形式自动装配 -->
	<context:annotation-config />
</beans>

1.2.1.2  业务类

业务类内容不变,只是要多加几个注解

public class UserService {
    private IUserDao userDao;

    @Autowired
    public void setUserDao(IUserDao userDao) {
        this.userDao = userDao;
    }
}

1.2.1.3  知识点

@Autowired(自动封装)

该注解可以加在set方法上或者直接加在属性上,如果写在setter方法上,就会通过setter方法进行注入,如果写在变量上,就直接通过反射设置变量的值,不经过setter方法。

注入时,会从spring容器中,找到一个和这个属性数据类型匹配的实例化对象注入进来,默认使用byType,根据类型匹配。

如果只能找到一个这个数据类型的对象的时候,就直接注入该对象。

如果找到了多个同一个类型的对象的时候,就会自动更改为byName来进行匹配,根据set方法对应的参数列表的局部变量名来匹配。

如:

private IUserDao userDao;
@Autowired
public void setUserDao(IUserDao userDao){};
会先找符合IUserDao类型的对象有多少,一个的话就直接拿过来
多个的话,就按照setUserDao方法的参数列表的局部变量名来找
找不到就报错
@Autowired(required=false) 就说明 这个值可以为null,如果Spring容器中没有对应的对象,不会报错
默认为true,比如beans.xml中没有创建dao对象,就会报错,加上required=false就不会报错

@Qualifier :  

以指定名字进行匹配  

private IUserDao userDao;
@Autowired
public void setUserDao(@Qualifier(“userDao2”)IUserDao userDao){};
这时候就不会按照userDao来进行匹配了,而是强制使用userDao2来进行比配,也就不会按照类型匹配了

1.2.1.4  配置文件

注意增加xml解析器内容,即头部beans内容

<?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">
	<!-- 引入注解约束 -->
	<!-- 使用注解形式自动装配 -->
	<context:annotation-config />
	<bean name="userDao" class="com.tledu.dao.impl.UserDaoImpl">
  <property name="daoId" value="1"></property>
	</bean>
  <bean name="userDao2" class="com.tledu.dao.impl.UserDaoImpl">
  <property name="daoId" value="2"></property>
	</bean>
	<bean id="userService" class="com.tledu.service.UserService">
	</bean>
</beans>

1.2.1.5  测试

@Test
public void testAdd() {
  ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
    "beans.xml");
  // userService是bean的id或name
   UserService userService = (UserService) applicationContext
   .getBean("userService");
   System.out.println(userService.getUserDao());
}

因为创建了两个对象,所以Dao构造方法执行两次

由于代码中定义了根据name匹配,所以最终找到的是daoId=2的对象

1.2.2  Resource

Resource这个注解是javaEE的,在javax包下,所以不需要导入其他jar包

@Resource默认使用byName的方式,按照名字匹配,可以写在setter方法上也可以写在变量上

先匹配set方法的名字,匹配不上再匹配方法参数列表的名字

如果还是匹配不上就会转换为byType,根据类型匹配

当然我们也可以指定名字

@Resource(name=”userDao”)  

就相当于 Autowired和Qualifier 一起使用

相关的还有一个 @Inject 根据type匹配,通过named指定名字,自行学习

1.2.2.1  业务类

@Resource(name="userDao")
  public void setUserDao(UserDao userDao) {
  	System.out.println("--------------");
  	this.userDao2 = userDao;
 }

1.2.2.2  测试

@Test
public void testAdd() {
  ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
    "beans.xml");
   UserService userService = (UserService) applicationContext
   .getBean("userService");
   System.out.println(userService.getUserDao());
 }

1.3  注解实例化使用步骤

上面的两个注解是用来进行注入对象的,实例化对象也有对应的注解,先来个简单示例进行了解

1.3.1  配置文件

创建项目和导包和上面的一样,业务类也一致,可以复制过来进行更改

在配置文件中设置使用注解形式实例化对象 只需要加入

<context:component-scan base-package="com.tledu"/

<?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">
	<!-- 使用注解形式自动装配 -->
	<context:annotation-config />
	<!-- 使用注解形式实例化对象 -->
	<context:component-scan base-package="com.tledu" />
</beans>

1.3.2  业务类

所有需要实例化对象的类上面都加上@Component  

默认是以类名首字母小写作为名字进行存储

可以使用@Component(“xxx”) 或者@Component(value=”xxx”)来设置名字

以上这三种写法都可以

@Component(value="userDao")
public class UserDaoImpl implements UserDao {
@Component
public class User {
@Component("userService")
public class UserService {

1.3.3  测试

@Test
public void testAdd() {
  ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
    "beans.xml");
  // userService是value设置的值或者是类名首字母小写
   UserService userService = (UserService) applicationContext
   .getBean("userService");
   System.out.println(userService.getUserDao());
}

1.3.4  注解分类

像上面我们写的代码中,所有实例化对象都是要的是@Component 这样不是很好,官方给出了几个分类

@Controller :WEB 层 ,就是和页面交互的类

@Service :业务层 ,主要处理逻辑

@Repository :持久层 ,就是Dao操作数据库

这三个注解是为了让标注类本身的用途清晰,Spring 在后续版本会对其增强  

@Component: 最普通的组件,可以被注入到spring容器进行管理

@Value :用于注入普通类型. 可以写在变量上和setter方法上

@Autowired :自动装配,上面描述比较详细,可以参照上面

@Qualifier:强制使用名称注入.  

@Resource 相当于: @Autowired 和@Qualifier 一起使用

@Scope: 设置对象在spring容器中的生命周期

取值 :  

singleton:单例  

prototype:多例

@PostConstruct :相当于 init-method  

@PreDestroy :相当于 destroy-method  

1.3.5  注解区别

引用spring的官方文档中的一段描述:

在Spring2.0之前的版本中,@Repository注解可以标记在任何的类上,用来表明该类是用来执行与数据库相关的操作(即dao对象),并支持自动处理数据库操作产生的异常

在Spring2.5版本中,引入了更多的Spring类注解:@Component,@Service,@Controller。@Component是一个通用的Spring容器管理的单例bean组件。而@Repository, @Service, @Controller就是针对不同的使用场景所采取的特定功能化的注解组件。

因此,当你的一个类被@Component所注解,那么就意味着同样可以用@Repository, @Service, @Controller来替代它,同时这些注解会具备有更多的功能,而且功能各异。

最后,如果你不知道要在项目的业务层采用@Service还是@Component注解。那么,@Service是一个更好的选择。

就如上文所说的,@Repository早已被支持了在你的持久层作为一个标记可以去自动处理数据库操作产生的异常(译 者注:因为原生的java操作数据库所产生的异常只定义了几种,但是产生数据库异常的原因却有很多种,这样对于数据库操作的报错排查造成了一定的影响;而 Spring拓展了原生的持久层异常,针对不同的产生原因有了更多的异常进行描述。所以,在注解了@Repository的类上如果数据库操作中抛出了异常,就能对其进行处理,转而抛出的是翻译后的spring专属数据库异常,方便我们对异常进行排查处理)。

注解 含义

@Component 最普通的组件,可以被注入到spring容器进行管理

@Repository 作用于持久层

@Service 作用于业务逻辑层

@Controller 作用于表现层(spring-mvc的注解)

1.4  新注解

1.4.1、Configuration

作用: 用于指定当前类是一个 spring 配置类,当创建容器时会从该类上加载注解。获取容器时需要

使用AnnotationApplicationContext(有@Configuration 注解的类.class)。

属性: value:用于指定配置类的字节码

package com.tledu.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfiguration {
 
}

注意:

我们已经把配置文件用类来代替了,但是如何配置创建容器时要扫描的包呢?请看下一个注解。

1.4.2、ComponentScan

作用: 用于指定 spring 在初始化容器时要扫描的包。

作用和在 spring 的 xml 配置文件中的:

是一样的。

属性: basePackages:用于指定要扫描的包。和该注解中的 value 属性作用一样。

@Configuration
@ComponentScan("com.tledu.spring")
public class SpringConfiguration {
 
}

注意: 我们已经配置好了要扫描的包,但是数据源和 JdbcTemplate 对象如何从配置文件中移除呢? 请看下一个注解。

1.4.3、Bean

作用: 该注解只能写在方法上,表明使用此方法创建一个对象,并且放入 spring 容器。

属性: name:给当前@Bean 注解方法创建的对象指定一个名称(即 bean 的 id)。

package com.tledu.zrz.config;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
public class JdbcConfig {
/**
 * 创建一个数据源,并存入 spring 容器中
 * @return
 */
     @Bean(name = "dataSource")
    public BasicDataSource createDataSource() {
    try {
        BasicDataSource bds = new BasicDataSource();
        bds.setDriverClassName("com.mysql.jdbc.Driver");
        bds.setUrl("jdbc:mysql://localhost:3306/ssm");
        bds.setUsername("root");
        bds.setPassword("root");
        return bds;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
}

注意:

由于没有配置文件,创建数据源的配置又都写死在类中了。如何把它们配置出来呢?

请看下一个注解。

1.4.4 PropertySource

作用: 用于加载.properties 文件中的配置。例如我们配置数据源时,可以把连接数据库的信息写到properties 配置文件中,就可以使用此注解指定 properties 配置文件的位置。

属性: value[]:用于指定 properties 文件位置。如果是在类路径下,需要写上 classpath

package com.tledu.config;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
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;
/**
 * 创建一个数据源,并存入 spring 容器中
 *
 * @return
 */
@Bean(name = "dataSource")
public BasicDataSource createDataSource() {
    try {
        BasicDataSource bds = new BasicDataSource();
        bds.setDriverClassName(driver);
        bds.setUrl(url);
        bds.setUsername(username);
        bds.setPassword(password);
        return bds;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
}
jdbc.properties文件
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/test
    jdbc.username=root
    jdbc.password=root

注意:

此时我们已经有了两个配置类,但是他们还没有关系。如何建立他们的关系呢?

请看下一个注解。

1.4.5 Import

作用: 用于导入其他配置类,在引入其他配置类时,可以不用再写@Configuration 注解。当然,写上也没问题。

属性: value[]:用于指定其他配置类的字节码。

@Configuration
@ComponentScan("com.tledu.spring")
@Import({ JdbcConfig.class})
public class SpringConfiguration {
 
 
@Configuration
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {

2、AOP(面向切面编程)

Spring 是解决实际开发中的一些问题,而 AOP 解决 OOP 中遇到的一些问题.是 OOP 的延续和扩展.

使用面向对象编程 ( OOP )有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志、安全检测等,我们只有在每个对象里引用公共行为,这样程序中就产生了大量的重复代码,程序就不便于维护了,所以就有了一个对面向对象编程的补充,即面向方面编程 ( AOP ), AOP 所关注的方向是横向的,区别于 OOP 的纵向。

2.1 为什么学习 AOP  

在不修改源码的情况下,对程序进行增强

AOP 可以进行权限校验,日志记录,性能监控,事务控制

2.2 Spring 的 AOP 的由来

AOP 最早由 AOP 联盟的组织提出的,制定了一套规范.Spring 将 AOP 思想引入到框架中,必须遵守 AOP 联盟 的规范.  

2.3 底层实现

AOP依赖于IOC来实现,在AOP中,使用一个代理类来包装目标类,在代理类中拦截目标类的方法执行并织入辅助功能。在Spring容器启动时,创建代理类bean替代目标类注册到IOC中,从而在应用代码中注入的目标类实例其实是目标类对应的代理类的实例,即使用AOP处理目标类生成的代理类。

代理机制:  

Spring 的 AOP 的底层用到两种代理机制:  

JDK 的动态代理 :针对实现了接口的类产生代理.  

Cglib 的动态代理 :针对没有实现接口的类产生代理. 应用的是底层的字节码增强的技术 生成当前类的子类对象.

2.4动态代理

package com.tledu.dao.proxy;

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

public class DynamicProxy implements InvocationHandler {
    private Object obj;
    public DynamicProxy(Object obj){
        this.obj = obj;
    }

    /**
     * @param proxy  代理的对象
     * @param method 代理的方法
     * @param args   方法的参数
     * @return 方法的返回值
     * @throws Throwable 异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //前置操作
        before(method);
        method.invoke(obj,args);
        //后置操作
        after(method);
        return null;
    }

    private void before(Method method) {
        System.out.println(method.getName()+" 开始调用");
    }
    private void after(Method method) {
        System.out.println(method.getName()+" 调用结束");
    }
}

测试

public static void main(String[] args) {
    UserDaoImpl userDao = new UserDaoImpl();
     /**
      第一个参数: 真实对象的类解析器
      第二个参数: 实现的接口数组
      第三个参数: 调用代理方法的时候,会将方法分配给该参数
     */
    IUserDao iu = (IUserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
            userDao.getClass().getInterfaces(),new DynamicProxy(userDao));
    User u = new User();
    u.setId(3);
    u.setName("aaa");
    iu.add(u);
}

newProxyInstance,方法有三个参数:

loader: 用哪个类加载器去加载代理对象

interfaces:动态代理类需要实现的接口

InvocationHandler: 传入要代理的对象创建一个代理,动态代理方法在执行时,会调用创建代理类里面的invoke方法去执行

注意:

JDK动态代理的原理是根据定义好的规则,用传入的接口创建一个新类,这就是为什么采用动态代理时为什么只能用接口引用指向代理,而不能用传入的类引用执行动态类。

2.5 AOP相关术语

  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为

spring 只支持方法类型的连接点.  

  • Advice(通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知.通知分为前置

通知,后置 通知,异常通知,最终通知,环绕通知(切面要完成的功能)  

  • Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
  • Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行

期为类 动态地添加一些方法或 Field.  

  • Target(目标对象): 代理的目标对象  
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程,spring 采用动态代理

织入,而 AspectJ 采用编译期织入和类装在期织入  

  • Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类  
  • Aspect(切面): 是切入点和通知(引介)的结合

2.6 注解方式

2.6.1 引入jar包

还是之前IOC的 再次引入三个就行,因为Spring的AOP是基于AspectJ开发的

0

2.6.2 配置文件

  1. 引入AOP约束,通过配置文件

<?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-2.5.xsd">
	<!-- 引入AOP约束 -->
	<!-- 使用注解方式 -->
	<context:annotation-config />
	<context:component-scan base-package="com.tledu" />
	<!-- 开启AOP的注解方式 -->
	<aop:aspectj-autoproxy />
</beans>

2.通过注解的方式开启切面支持 @EnableAspectJAutoProxy

2.6.3 AOP类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogInterceptor {
      @Pointcut("execution(public * com.tledu.service..*.add(..))")
      public void myMethod() {
          // 假设这个方法就是被代理的方法
      }
      @Before("myMethod()")
      public void beforeMethod() {
          System.out.println(" execute start");
      }
      @After("myMethod()")
      public void afterMethod() {
          System.out.println(" execute end");
      }
      // 环绕,之前和之后都有,相当于 @Before 和 @After 一起使用
      @Around("myMethod()")
      public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
      // @Around也可以和@Before 和 @After 一起使用
          System.out.println("around start");
          // 调用被代理的方法
          pjp.proceed();
          System.out.println("around end");
      }
}

2.7 XML方式

2.7.1 引入jar包

和注解方式引入的一样

<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-2.5.xsd">
    <context:annotation-config/>
    <context:component-scan base-package="com.tledu"/>
    <!--开启AOP注解-->
    <!--<aop:aspectj-autoproxy />-->

    <bean id="logInterceptor" class="com.tledu.aop.LogInterceptor"></bean>
    <aop:config>
        <aop:aspect id="logAspet" ref="logInterceptor">
            <!-- 需要进行代理的方法 -->
            <aop:pointcut expression="execution(public * com.tledu.service..*.add(..))"
                          id="pointcut" />
            <!-- 前后执行 -->
            <aop:before method="beforeMethod" pointcut-ref="pointcut" />
            <!-- 或者这样写,这样就不需要pointcut标签了 -->
            <!-- <aop:before method="before" pointcut="execution(public * com.tledu.service..*.add(..))"
          /> -->
            <aop:after method="afterMethod" pointcut-ref="pointcut" />
            <!-- 环绕,一般要么使用 around 要和使用 before和after 不会一起使用 -->
            <aop:around method="aroundMethod" pointcut-ref="pointcut" />
        </aop:aspect>
    </aop:config>

</beans>
2.6.3 AOP类

2.6.3 AOP类

和注解一样,就是把注解去掉了

import org.aspectj.lang.ProceedingJoinPoint;
public class LogInterceptor {
public void myMethod() {
 // 假设这个方法就是被代理的方法
}
public void before() {
 System.out.println(" execute start");
}
public void after() {
 System.out.println(" execute end");
}
// 环绕,之前和之后都有,相当于 @Before 和 @After 一起使用
public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
 // @Around也可以和@Before 和 @After 一起使用
 System.out.println("around start");
 // 调用被代理的方法
 pjp.proceed();
 System.out.println("around end");
}
}

2.6.4 测试类

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
            "applicationContext.xml");
    UserService userService = context.getBean(UserService.class);
    User u = new User();
    u.setName("李四");
    userService.add(u);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Iiversse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值