3.如何实现AOP?(静态代理,Jdk动态代理,cglib动态代理,自定义动态代理)

1.什么是AOP?

AOP 中文名称为面向切面编程,英文全称:Aspect Oriented Programming。
是一种通过预编译或运行期间动态代理实现程序功能的一种编程,简单来说就是一种高级的代码复用技术,它既实现了代码高度复用,也一一种特殊的方式使复用部分的影响降到很低,甚至没有。既增加了功能,又没有增加耦合。

2.实现一个自己的AOP支持

前面的解释也许还是有点抽象,下面我们以实际的例子来理解什么面向切面编程,我们将通过一次需求的程序实现和代码优化来引出切面编程技术的必要性。

2.1 有个需求

我们现在有两个类,一个是学生实体类,另一个是教师实体类:

public class Student {
    private String name;
    private int age;
    private String sex;
    public Student(){}
    public Student(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void disPlay() {
        System.out.println("我的名字是:"+name);
    }
    public void speak(){
        System.out.println("你好!!")    
    }
}

public class Teacher {
    private String name;
    private int age;
    private String sex;
    public Teacher(){}
    public Teacher(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void disPlay() {
        System.out.println("我的名字是:"+name);
    }
    public void speak(){
        System.out.println("你好!!")    
    }
}

哎呀,这可是个糟糕的设计,很明显,教师类和学生类几乎一样,代码重复率高达90%,不行,我们得重新设计一下,于是,想到了继承,通过继承来提高代码的重用性,这个方式怎么样呢? 我们先试试吧
首先,我们先建立一个Person类:

public class Person {
    public String name;
    public int age;
    public String sex;
    public Person(){}
    public Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void disPlay() {
        System.out.println("我的名字是:"+name);
    }
    public void speak(){
        System.out.println("你好!!");
    }
}

然后,学生类继承它,是这样的:

public class Student extends Person{
    public Student(){}
    public Student(String name,int age,String sex){
        super(name,age,sex);
    }
}

类似的老师类是这样的:

public class Teacher extends Person{
    public Teacher(){}
    public Teacher(String name,int age,String sex){
        super(name,age,sex);
    }
}

我们发现,这样做确实能将重复的代码给抹掉,但是,这样一来,学生,老师类和Person类的耦合度也上来了啊,我们又进入了侵入式编程噩梦。。。。
我们该如何摆脱现状呢? 这时,AOP出现了,面向切面编程,我们可以通过面向切面编程来避免上面的侵入式噩梦。

2.2 真正理解AOP

那么问题来了,我们自己该如何实现一个简单的AOP技术支持呢? 通过前面的定义我们知道了:
AOP是一种通过预编译或运行期间动态代理实现程序功能的一种编程技术。 其中一个关键字: 运行时期动态代理。
也许这是我们的一个实现方向。

2.2.1 我们先了解一下什么是代理?

代理,简单来说就是将一个类的某些功能组合到其他的类中,让其他类代替主类调用来达到一些目的。
代理分为静态代理和动态代理,下面先说静态代理,理解了静态代理,动态代理也就好理解了。(
由于动态代理机制在jdk中已经提供,后面学习动态代理时,我们将直接使用jdk的相关api即可)

举个例子:
新建一个Hello.java

public class Hello{
    public Integer sayHello() {
        System.out.println("hello world!!");
        return 8;
    }
    public void sayBye() {
        System.out.println("BBye");
    }
}

如果我们不想让这个Hello的sayHello直接被调用,而是想在调用这个sayHello之前希望先做个自我介绍,但是由不想动原有的代码,这时我们可以增加一个代理类:HelloProxy 代替我们调用Hello中的sayHello

public class HelloProxy {
    private  Hello hello;//组合
    public Integer sayHello() {
        System.out.println("my name is wang");
        hello.sayHello();//代理调用
        return 8;
    }
    public HelloProxy(HelloInterface hello){
        this.hello = hello;
    }
}

这便是代理了。

public class Main {
    public static void main(String[] args) {
        new HelloProxy(new Hello()).sayHello();
    }
}

在这里插入图片描述

但是上面的代理类还是依赖具体了,这在实际上并达不到我们的目的,还有更好的实现,你觉得该怎么做呢?
(我们不应该依赖具体,而应该依赖抽象)

2.2.2 通过静态代理实现代码复用

好了,前面学习了静态代理,我们尝试使用静态代理来解决我们最初要解决的问题。
首先,我们可以新建一个接口:personInterface

public interface personInterface {
    public void disPlay();
    public void speak();
}

然后让Person实现它:

public class Person implements personInterface{
    public String name;
    public int age;
    public String sex;
    public Person(){}
    public Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void disPlay() {
        System.out.println("我的名字是:"+name);
    }
    public void speak(){
        System.out.println("你好!!");
    }
}

好啦,下面我们该如何编写Student或Teacher呢?
参考设计如下:

public class Student{
    private personInterface person;
    public int studyId;
    public Student(){}
    public Student(personInterface person){
      this.person = person;
    }
    public void disPlay() {
      person.disPlay();
    }
    public void speak(){
      person.speak();
    }
}

终于,通过静态代理解决了侵入式的问题,但是好像也只是省略了一点代码,并没有达到极致复用,还是有一些地方冗余了,就是那些方法的定义上。这个问题能解决吗? 当然可以,通过动态代理,我们可以解决

2.2.2 动态代理

我们先来实现一个动态代理的Demo:
首先,新建一个Hello的接口:

public interface HelloInterface {
    Integer sayHello();
    void sayBye();
}

然后提供一个实现:

public class Hello implements HelloInterface{
    @Override
    public Integer sayHello() {
        System.out.println("hello world!!");
        return 8;
    }
    @Override
    public void sayBye() {
        System.out.println("BBye");
    }
}

JDK自带代理支持
下面是用动态代理来对Hello进行代理调用,建立一个代理类:
ProxyHandler 实现jdk中的接口InvocationHandler

/**
 * 动态增强操作对象
 */
public class ProxyHandler implements InvocationHandler {
    //被代理对象
    private Object object;

    public ProxyHandler(Object object){
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
}

其中invoke方法是关键,我们先不细看,下面会根据使用实例来进行深入理解。
最后,我们编写一个测试Test

public class Test {
    public static void main(String[] args) {

        HelloInterface hello = new Hello();

        InvocationHandler handler = new ProxyHandler(hello);

       //Class[] classes = hello.getClass().getInterfaces();

        //执行被代理对象的相关逻辑
        HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

        proxyHello.sayHello();

        proxyHello.sayBye();

    }
}

在这里插入图片描述

我们可以看到,最后对代理目标方法的调用实际上就是执行代理类中的invoke方法调用,其中对目标方法的调用被包装在代理类中,我们可以在代理类执行目标方法之前或之后添加前置/后置处理,这便是AOP。 好处就是以一种很小的侵入式方式实现目标方法功能增强。

CGLIB代理支持
当然,除了上面的JDK动态代理支持来实现,我们也可以用一种绝对无侵入的方式进行动态代理编程,那便是CGLIB:
注意:使用CGLIB进行动态代理编程,我们需要引入cglib.jar包。
例子如下:
首先编写一个目标类:

public class UserDao {
    public void select(){
        System.out.println("UserDao 查询 selectById");
    }

    public void update(){
        System.out.println("UserDao 更新 update");
    }
}

然后编写针对这个类的前置后置处理实现类:

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class LogInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(o,objects);
        after();
        return result;
    }

    public void before(){
        System.out.println("前置处理");
    }
    public void after(){
        System.out.println("后置处理");
    }
}

注意,上面两个类没有任何继承或组合的关系!!
下面我们使用CGlib来进行生产代理类(相当于配置的方式):

import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
    public static void main(String[] args) {
        LogInterceptor logInterceptor = new LogInterceptor();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserDao.class);
        enhancer.setCallback(logInterceptor);
        UserDao dao = (UserDao) enhancer.create();
        dao.select();
        dao.update();
    }
}

在这里插入图片描述

这种方式的优点就是无侵入,很灵活。

2.3 实现自己的AOP技术支持

在学习完前面的静态代理和动态代理后,我突然有了以下认知:
静态代理就是我们人为的创建代理类然后进行使用。
动态代理就是在一些规则下根据配置在运行时动态的生成代理类供系统使用。
根据这个思路,我就想:
我自己能不能写一个代理类自动生成器可以根据配置自动生成java源文件,然后我们根据java提供的内置编译器对其进行编译加载,然后创建实例供系统使用呢?
想法有了,于是我就开始构思和实现了,终于,事实证明我的想法时可行的,下面分享下我自己实现的动态代理实现:
涉及到的技术主要有: 反射,编译器,类加载器等知识,大家可以先复现,然后自己去仔细研究。
代码如下:

CharSequenceJavaFileObject.java

package AOP.base.ClassMake;

import javax.tools.SimpleJavaFileObject;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;

public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    public static final String CLASS_EXTENSION = ".class";

    public static final String JAVA_EXTENSION = ".java";

    private static URI fromClassName(String className) {
        try {
            return new URI(className);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(className, e);
        }
    }

    private ByteArrayOutputStream byteCode;
    private final CharSequence sourceCode;

    public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
        super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
        this.sourceCode = sourceCode;
    }

    public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
        super(fromClassName(fullClassName), kind);
        this.sourceCode = null;
    }

    public CharSequenceJavaFileObject(URI uri, Kind kind) {
        super(uri, kind);
        this.sourceCode = null;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return sourceCode;
    }

    @Override
    public InputStream openInputStream() {
        return new ByteArrayInputStream(getByteCode());
    }

    // 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
    @Override
    public OutputStream openOutputStream() {
        return byteCode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
        return byteCode.toByteArray();
    }
}

JdkDynamicCompileClassLoader.java

package AOP.base.ClassMake;

import com.sun.istack.internal.Nullable;

import javax.tools.JavaFileObject;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final Map<String, JavaFileObject> javaFileObjectMap = new HashMap<>();

    public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(name);
        if (null != javaFileObject) {
            CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
            byte[] byteCode = charSequenceJavaFileObject.getByteCode();
            return defineClass(name, byteCode, 0, byteCode.length);
        }
        return super.findClass(name);
    }

    @Nullable
    @Override
    public InputStream getResourceAsStream(String name) {
        if (name.endsWith(CLASS_EXTENSION)) {
            String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
            CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
            if (null != javaFileObject && null != javaFileObject.getByteCode()) {
                return new ByteArrayInputStream(javaFileObject.getByteCode());
            }
        }
        return super.getResourceAsStream(name);
    }

    /**
     * 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
     */
    void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(qualifiedClassName, javaFileObject);
    }

    Collection<JavaFileObject> listJavaFileObject() {
        return Collections.unmodifiableCollection(javaFileObjectMap.values());
    }
}

JdkDynamicCompileJavaFileManager.java

package AOP.base.ClassMake;

import javax.tools.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap =new HashMap<>();

    public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
        super(fileManager);
        this.classLoader = classLoader;
    }

    private static URI fromLocation(Location location, String packageName, String relativeName) {
        try {
            return new URI(location.getName() + '/' + packageName + '/' + relativeName);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
        if (null != javaFileObject) {
            return javaFileObject;
        }
        return super.getFileForInput(location, packageName, relativeName);
    }

    /**
     * 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
        classLoader.addJavaFileObject(className, javaFileObject);
        return javaFileObject;
    }

    /**
     * 这里覆盖原来的类加载器
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return classLoader;
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
        if (file instanceof CharSequenceJavaFileObject) {
            return file.getName();
        }
        return super.inferBinaryName(location, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
        Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
        List<JavaFileObject> result = new ArrayList<>();
        // 这里要区分编译的Location以及编译的Kind
        if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
            // .class文件以及classPath下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
            // 这里需要额外添加类加载器加载的所有Java文件对象
            result.addAll(classLoader.listJavaFileObject());
        } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
            // .java文件以及编译路径下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
        }
        for (JavaFileObject javaFileObject : superResult) {
            result.add(javaFileObject);
        }
        return result;
    }

    /**
     * 自定义方法,用于添加和缓存待编译的源文件对象
     */
    public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
    }
}

ProxySourceMaker.java

package AOP.base.SelfProxy;

import AOP.base.ClassMake.CharSequenceJavaFileObject;
import AOP.base.ClassMake.JdkDynamicCompileClassLoader;
import AOP.base.ClassMake.JdkDynamicCompileJavaFileManager;

import javax.tools.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

/**
 *自定义代理类生成器,实现切面AOP技术支持
 */
public class ProxySourceMaker {

    private Object proxy;

    static String SOURCE_CODE = "";

    private String packageName;

    private String className;

    /**
     * 编译诊断收集器
     */
    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();


    ProxySourceMaker(Class<?> handlerClass,Class<?> targetClass){
        String packageName = targetClass.getPackage().getName();
        this.packageName = packageName;
        String className = handlerClass.getSimpleName()+targetClass.getSimpleName();
        this.className = className;
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("package").append(" ").append(packageName).append(";").append("\n");
        stringBuilder.append("import ").append(targetClass.getName()).append(";").append("\n");
        stringBuilder.append("import ").append(handlerClass.getName()).append(";").append("\n");
        stringBuilder.append("public class ").append(className).append(" extends ").append(targetClass.getName()).append("{\n");
        //组合处理器对象
        stringBuilder.append(handlerClass.getSimpleName()).append(" ").append(handlerClass.getSimpleName()).
                append(" = ").append("new ").append(handlerClass.getSimpleName()).append("();\n");

        Method[] methods = targetClass.getDeclaredMethods();

        for(Method method:methods){
            stringBuilder.append("@Override").append("\n");
            int m = method.getModifiers();
            //方法修饰符
            stringBuilder.append(Modifier.toString(m)).append(" ");
            //方法返回类型
            stringBuilder.append(method.getReturnType().getName()).append(" ");
            //方法名称
            stringBuilder.append(method.getName()).append("( ");
            //方法参数
            Class<?>[] params = method.getParameterTypes();
            int i=0;
            if(params.length>0){
                for(Class<?> cl:params){
                    stringBuilder.append(cl.getName()).append(" param").append(i++).append(" ");
                }
            }
            stringBuilder.append(")").append("{").append("\n");
            //调用前置处理
            stringBuilder.append(handlerClass.getSimpleName()).append(".before();\n");
            String result = "";
            if(!method.getReturnType().getName().equals("void")){
                stringBuilder.append(method.getReturnType().getName()).append(" ").append("result;").append("\n");
                stringBuilder.append("result = ");
                result = "result";
            }

            //调用目标方法
            stringBuilder.append("super.").append(method.getName()).append("( ");
            for(int j=0;j<i;j++){
                stringBuilder.append("param").append(j).append(" ");
            }
            stringBuilder.append(");").append("\n");

            //调用后置处理
            stringBuilder.append(handlerClass.getSimpleName()).append(".after();\n");
            stringBuilder.append("return ").append(result).append(";").append("\n");

            stringBuilder.append("}").append("\n");
        }

        stringBuilder.append("}\n");

        SOURCE_CODE = stringBuilder.toString();

    }


    public Object getProxy() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取编译器实例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 设置编译参数 - 指定编译版本为JDK1.6以提高兼容性
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        // 获取标准的Java文件管理器实例
        StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
        // 初始化自定义类加载器
        JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
        // 初始化自定义Java文件管理器实例
        JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
        String packageName = this.packageName;
        String className = this.className;
        String qualifiedName = packageName + "." + className;
        // 构建java文件对象字符序列
        CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, SOURCE_CODE);
        // 添加Java源文件实例到自定义Java文件管理器实例中
        fileManager.addJavaFileObject(
                StandardLocation.SOURCE_PATH,
                packageName,
                className + CharSequenceJavaFileObject.JAVA_EXTENSION,
                javaFileObject
        );

        // 初始化一个编译任务实例并执行编译任务
        List<JavaFileObject> list = new ArrayList<>();
        list.add(javaFileObject);
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(
                null,
                fileManager,
                DIAGNOSTIC_COLLECTOR,
                options,
                null,
                list
        );
        Boolean result = compilationTask.call();
        //打印编译结果
        System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
        //加载类获得Class实例
        Class<?> klass = classLoader.loadClass(qualifiedName);
        //根据class实例实例化对象
        proxy = klass.getDeclaredConstructor().newInstance();

        return proxy;
    }
}

ok,到此,已经实现了动态代理支持,下面进行使用。我们开始建立一个目标类:

public class Hello implements HelloInterface{

    @Override
    public Integer sayHello() {
        System.out.println("hello world!!");
        return 8;
    }

    @Override
    public void sayBye() {
        System.out.println("BBye");
    }

    @Override
    public void sayHei() {

    }

    @Override
    public void jojo() {

    }
}

然后建立一个处理器类(前置,后置处理):

public class SelfHandler {
    public void before(){
        System.out.println("前置处理");
    }
    public void after(){
        System.out.println("后置处理");
    }
    public void around(){
        System.out.println("环绕处理");
    }
}

最后,进行测试,看通过上面两个类能不能生成一个代理类:

import AOP.base.Hello;
import java.lang.reflect.InvocationTargetException;
public class SelfTest {
    public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        Hello hello = (Hello)new ProxySourceMaker(SelfHandler.class,Hello.class).getProxy();
        hello.sayHello();
    }
}

在这里插入图片描述

成功!!!!
好了,如果你成功复现出来了,那么恭喜,你即将触及动态代理的精髓,下面就仔细去研读代码吧,如果有想法,就去优化,争取做一个更好的AOP框架出来。

项目代码地址:
https://gitee.com/yan-jiadou/design-mode/tree/master/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/src/main/java/AOP/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员小牧之

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

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

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

打赏作者

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

抵扣说明:

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

余额充值