spring mvc-⼿写 spring mvc 框架

前言

本文通过手写实现 类似spring mvc的简单框架。

spring mvc 请求流程图
在这里插入图片描述
在这里插入图片描述

思路分析

要实现自定义的spring mvc,最核心的就是怎么让请求在doGet和doPost方法进行处理时,根据请求的路径找到对应的方法去执行。
maven 引入jar配置

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>spring-myMvc</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>spring-myMvc Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>


    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.9</version>
    </dependency>
  </dependencies>



  <build>
    <plugins>
      <!--编译插件定义编译细节-->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <encoding>utf-8</encoding>
          <!--告诉编译器,编译的时候记录下形参的真实名称-->
          <compilerArgs>
            <arg>-parameters</arg>
          </compilerArgs>
        </configuration>
      </plugin>


      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
          <port>8080</port>
          <path>/</path>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

自定义注解

自定义注解类实现容器注入,被注解的类都需要被自定义的容器所管理

MyAutowired 用于属性类类型对象注入

package com.my.test.annotations;

import java.lang.annotation.*;


/**
 * @author Administrator
 */
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAutowired {

    /**
     * Declares whether the annotated dependency is required.
     * <p>Defaults to {@code true}.
     */
    boolean required() default true;

    String value() default "";

}

MyComponent 用于标识bean是否需要被容器管理

package com.my.test.annotations;

import java.lang.annotation.*;

/**
 * @author Administrator
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyComponent {

	String value() default "";
}

MyComponent 用于标识被容器管理bean是控制类

/**
 * @author Administrator
 */
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@MyComponent
public @interface MyController {
    String value() default "";
}

MyRequestMapping 用于维护路径与控制类或者方法的映射关系


import java.lang.annotation.*;

/**
 * @author Administrator
 */
@Documented
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestMapping {
    String value() default "";
}

MySecurity 用于对权限的控制

/**
 * @author Administrator
 * 该注解可以用在类上也可以用在方法上
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MySecurity {
    String[] value() default "";
    String name() ;
}

MyService 用于标识被容器管理bean是接口的实现类

/**
 * @author Administrator
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponent
public @interface MyService {

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     */
    String value() default "";

}

搭建三层架构 持久层不重点处理简单返回

DemoController

package com.my.test.controller;

import com.my.test.annotations.MyAutowired;
import com.my.test.annotations.MyController;
import com.my.test.annotations.MyRequestMapping;
import com.my.test.annotations.MySecurity;
import com.my.test.service.IDemoService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Administrator
 */
@MyController
@MyRequestMapping("/demo")
@MySecurity(name = "username",value ={"lisi","zhangsan","zhaoliu"})
public class DemoController {

    @MyAutowired
    private IDemoService demoService;


    /**
     * URL: /demo/query?name=李四
     * @param request
     * @param response
     * @param username
     * @return
     */
    @MyRequestMapping("/query1")
    @MySecurity(name = "username",value ={"zhangsan","zhaoliu"})
    public String query1(HttpServletRequest request, HttpServletResponse response,String username) {
        return demoService.get(username);
    }
    /**
     * URL: /demo/query?name=张三
     * @param request
     * @param response
     * @param username
     * @return
     */
    @MyRequestMapping("/query2")
    @MySecurity(name = "username",value = {"zhangsan","lisi"})
    public String query2(HttpServletRequest request, HttpServletResponse response,String username) {
        return demoService.get(username);
    }
    /**
     * URL: /demo/query?name=赵六
     * @param request
     * @param response
     * @param username
     * @return
     */
    @MyRequestMapping("/query3")
    @MySecurity(name = "username",value ={"lisi","zhaoliu"})
    public String query(HttpServletRequest request, HttpServletResponse response,String username) {
        return demoService.get(username);
    }

    /**
     * 都能访问
     * @param request
     * @param response
     * @param username
     * @return
     */
    @MyRequestMapping("/query4")
    public String query4(HttpServletRequest request, HttpServletResponse response,String username) {
        return demoService.get(username);
    }
}

接口和实现类的编写

/**
 * @author Administrator
 */
public interface IDemoService {

    String get(String name);
}

package com.my.test.service.impl;

import com.my.test.annotations.MyService;
import com.my.test.service.IDemoService;

/**
 * @author Administrator
 */
@MyService("demoService")
public class DemoServiceImpl implements IDemoService {
    @Override
    public String get(String name) {
        System.out.println("service 实现类中的name参数:" + name) ;
        return name+">>welcome";
    }
}

自定义MyDispatcherServlet

MyDispatcherServlet类需要做什么的分析

第一步配置web.xml文件 配置Servlet处理器是我们自定义的MyDispatcherServlet 以及拦截路径的配置和初始化需要加载文件的配置

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>

  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>myDispatcherServlet</servlet-name>
    <servlet-class>com.my.test.MyDispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>springmvc.properties</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>myDispatcherServlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

</web-app>

第二步编写配置文件springmvc.properties 配置需要扫描的包路径

scanPackage=com.my.test

第三步重写init(ServletConfig config) 方法实现 我们需要的逻辑
// 开始重写初始化方法加入自己的逻辑
// 1 加载配置⽂件 springmvc.properties 需要扫描的包路径
String contextConfigLocation = config.getInitParameter(“contextConfigLocation”);

        // 拿出配置文件中配置的路径值
        String path = doLoadConfig(contextConfigLocation);
        
        // 2. 找到需要扫描的包中需要放入ioc容器中的类的全路径 存放在classPath 集合中 
        doScan(path);
        
        // 3. 解析出bean并实例化放入自定义ioc容器中
        doInstance();
        
        // 4. 维护bean之间的关系 也就是通过set方法进行属性的注入
        doAutowired();
        
        // 5. 开始绑定请求路径与方法的对应关系 在后面请求的时候就能拿到需要执行的方法对象 通过反射执行调用。
        // 权限校验是就近原则  其实就是在调用方法前 判断传入的参数是否在能定义的数组中存在,任意存在一个就能访问
        initHandlerMapping();
        
        // 等待请求 处理请求

MyDispatcherServlet类的编写

在写之前我们要明白,所有的请求都是通过servlet来进行处理的,在spring mvc中是DispatcherServlet来进行请求的处理和转发,DispatcherServlet extends FrameworkServlet 然后 FrameworkServlet extends HttpServletBean 然后HttpServletBean extends HttpServlet所以最终都是落到了HttpServlet上。因此我们需要写一个类继承HttpServlet

package com.my.test;


import com.my.test.annotations.*;
import com.my.test.pojo.Handler;
import com.my.test.pojo.Visit;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 自定义 DispatcherServlet 来进行handlerMapping 和容器初始化
 * @author HuYuanLiang
 * @since 2021/5/29
 */
public class MyDispatcherServlet extends HttpServlet {

    /**
     * 封装流数据到properties中
     */
    Properties properties = new Properties();
    /**
     * ioc容器
     */
    public  Map<String,Object> iocMap = new ConcurrentHashMap<>(16);

    /**
     * 类全路径集合
     */
    public  List<String> classPath = new ArrayList<>(10);

    /**
     * handler处理器集合类
     */
    public  List<Handler> handlerList = new ArrayList<>(10);

    @Override
    public void init(ServletConfig config) throws ServletException {
        try {
            // 开始重写初始化方法加入自己的逻辑
            // 1 加载配置⽂件 springmvc.properties
            String contextConfigLocation =
                    config.getInitParameter("contextConfigLocation");
            String path = doLoadConfig(contextConfigLocation);
            // 2. 找到需要扫描的包中的类
            doScan(path);
            // 3. 解析出bean放入自定义ioc容器中
            doInstance();
            // 4. 维护bean之间的关系
            doAutowired();
            // 5. 开始绑定请求路径与方法的对应关系
            initHandlerMapping();
            // 等待请求 处理请求
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 开始生成请求路径与处理器的映射
     *  分析 如果我们直接定义一个map存放 路径与对应方法对象的映射 但是
     */
    private void initHandlerMapping() {
        // 如果不包含任何元素
        if (iocMap.isEmpty()) {
            return;
        }
        for (Object obj : iocMap.values()) {
            String[] visit = null;
            String visitKey = null;
            Class<?> aClass = obj.getClass();
            //请求路径
            StringBuilder path = new StringBuilder();
            // 如果类上面指定了路径
            if (aClass.isAnnotationPresent(MyRequestMapping.class)) {
                path.append(aClass.getAnnotation(MyRequestMapping.class).value());
            }
            // 如果类上面有权限 先拿出类中的权限
            if (aClass.isAnnotationPresent(MySecurity.class)) {
                visit = aClass.getAnnotation(MySecurity.class).value();
                visitKey = aClass.getAnnotation(MySecurity.class).name();
            }
            // 获取到类的所有方法对象
            Method[] declaredMethods = aClass.getDeclaredMethods();
            for (int i = 0; i < declaredMethods.length; i++) {
                Method declaredMethod = declaredMethods[i];
                if (declaredMethod.isAnnotationPresent(MyRequestMapping.class)) {
                    Handler handler = new Handler();
                    // 获取到方法上面的路径 开始拼接 /demo/query1
                    String value = declaredMethod.getAnnotation(MyRequestMapping.class).value();
                    // 获取方法上的权限 如果有就用方法上的为准  没有就用类上面的
                    if (declaredMethod.isAnnotationPresent(MySecurity.class)){
                        String[] methodVisitArray = declaredMethod.getAnnotation(MySecurity.class).value();
                        String methodKey = declaredMethod.getAnnotation(MySecurity.class).name();
                        Visit methodVisit = new Visit(methodKey,methodVisitArray);
                        handler.setVisit(methodVisit);
                    } else if(visit!=null && visitKey!=null){
                        Visit methodVisit = new Visit(visitKey,visit);
                        handler.setVisit(methodVisit);
                    }
                    // 设置类
                    handler.setControlObject(obj);
                    // 设置方法对象
                    handler.setMethod(declaredMethod);
                    // 设置路径
                    handler.setPattern(Pattern.compile(path+value));
                    Map<String,Integer> paramIndexMapping = new HashMap<>(16);
                    // 设置参数的角标和值  暂时不考虑非常复杂的情况
                    // 获取方法中需要传入的参数数组
                    Parameter[] parameters = declaredMethod.getParameters();
                    for (int i1 = 0; i1 < parameters.length; i1++) {
                        Parameter parameter = parameters[i1];
                        // 检查参数是否是封装后的对象
                        if (parameter.isAnnotationPresent(MyRequestBody.class) || parameter.getType()== HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {
                            paramIndexMapping.put(parameter.getType().getSimpleName(),i1);
                        } else {
                            paramIndexMapping.put(parameter.getName(),i1);
                        }
                    }
                    handler.setParamIndexMapping(paramIndexMapping);
                    handlerList.add(handler);
                }
            }
        }
    }

    /**
     * 维护类之间关系
     */
    private void doAutowired() throws Exception {
        // 如果不包含任何元素
        if (iocMap.isEmpty()) {
            return;
        }
        for (Object obj : iocMap.values()) {
            Class<?> aClass = obj.getClass();
            // 获取到类的所有属性对象
            Field[] declaredFields = aClass.getDeclaredFields();
            for (int i = 0; i < declaredFields.length; i++) {
                Field declaredField = declaredFields[i];
                // 判断属性对象头上是否包含注解MyAutowired
                if (declaredField.isAnnotationPresent(MyAutowired.class)) {
                    MyAutowired annotation = declaredField.getAnnotation(MyAutowired.class);
                    // 需要注入
                    if (annotation.required()) {
                        String name = annotation.value();
                        // 如果没有指定名称 按照接口类型注入
                        if (name.equals("")) {
                            // 获取到接口的全路径类名
                            name = declaredField.getType().getName();
                        }
                        // 从ioc中拿出来 如果为null则最开始实例化没有实例化完我们所需要的所有类
                        Object o = iocMap.get(name);
                        if (o==null) {
                            throw new RuntimeException("需要类<<"+name+">>不存在!");
                        }
                        declaredField.setAccessible(true);
                        declaredField.set(obj,o);
                    }
                }
            }
        }
    }

    /**
     * 初始化所有bean
     */
    private void doInstance() throws Exception {
        // 开始初始化话所有的bean
        for (String s : classPath) {
            Class<?> aClass = Class.forName(s);
            // 如果是MyController 注解 如果有别名就用别名 没有就用首字母小写的类名
            if (aClass.isAnnotationPresent(MyController.class)) {
                MyController annotation = aClass.getAnnotation(MyController.class);
                saveToIoc(annotation.value(),aClass);
            } else  if (aClass.isAnnotationPresent(MyService.class)) {
                MyService annotation = aClass.getAnnotation(MyService.class);
                saveToIoc(annotation.value(),aClass);
                // 一般service都会实现接口 将接口类名称和实现类存入ioc中方便后面注入和生成代理对象
                Class<?>[] interfaces = aClass.getInterfaces();
                for (Class<?> anInterface : interfaces) {
                    //  将接口类名称和实现类存入ioc中方便后面注入和生成代理对象
                    iocMap.put(anInterface.getName(),aClass.getDeclaredConstructor().newInstance());
                }
            }
        }
    }

    /**
     * 获取在ioc容器中的名称
     * @param className
     * @param aClass
     */
    private void saveToIoc(String className, Class<?> aClass) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        if (className.equals("")) {
            className = aClass.getSimpleName().substring(0,1).toLowerCase()+aClass.getSimpleName().substring(1);
        }
        iocMap.put(className, aClass.getDeclaredConstructor().newInstance());
    }


    /**
     * 加载配置并返回配置中的特定值的value
     * @param contextConfigLocation
     * @return
     * @throws IOException
     */
    private String doLoadConfig(String contextConfigLocation) throws IOException {
        if (contextConfigLocation.equals("")){
            return contextConfigLocation;
        }
        // 获取类加载器
        ClassLoader classLoader = this.getClass().getClassLoader();
        // 将文件加载到内存中
        InputStream resourceAsStream = classLoader.getResourceAsStream(contextConfigLocation);
        // 封装流数据到properties中
        properties.load(resourceAsStream);
        // 读取到需要扫描的包路径
        return properties.getProperty("scanPackage");
    }

    /**
     * 根据路径扫描出包下的类
     * @param path
     * 配置的com.my.test路径  我们需要找到真实的磁盘上的路径 然后查询是不是文件夹  递归进行解析查找
     */
    private void doScan(String path) {
        if (path.equals("")){
            return;
        }
        // 获取当前加载的磁盘根路径  path=com.my.test
        String s = Thread.currentThread().getContextClassLoader().getResource("").getPath() + path.replaceAll("\\.","/");
        File file = new File(s);
        // 获取所有的文件 开始循环
        File[] files = file.listFiles();
        for (File file1 : files) {
            // 如果是目录开始递归
            if (file1.isDirectory()) {
                // 开始组装新的路径用于递归
                // newPath = com.my.test.annotations
                String newPath =  path+"."+file1.getName();
                doScan(newPath);
            } else  if (file1.getName().endsWith(".class")) {
                // 如果获取到的是class 类
                // 获取出类的路径用于后面初始化bean    com.my.test.pojo.Handler.class
                classPath.add(path+"."+file1.getName().replace(".class",""));
            }
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取到请求路径 去
        String requestURI = req.getRequestURI();
        Handler handler = getHandler(requestURI);
        if(handler == null) {
            resp.getWriter().write("404 not found");
            return;
        }
        // 获取定义好的参数的key和位置
        Map<String, Integer> mapping = handler.getParamIndexMapping();

        // 获取出方法需要的参数类型数组
        Class<?>[] parameters = handler.getMethod().getParameterTypes();
        // 创建一个新数组装参数
        Object[] objects=new Object[parameters.length];
        // 获取出请求的map
        Map<String, String[]> parameterMap = req.getParameterMap();
        for (String s : parameterMap.keySet()) {
            if (mapping.containsKey(s)){
                objects[mapping.get(s)] = StringUtils.join(parameterMap.get(s),",");
            }
        }
        // 判断是否能够访问
        boolean haveVisit = canVisit(handler,parameterMap);
        if (!haveVisit) {
            resp.getWriter().write("403 Forbidden");
            return;
        }
        // 放请求对象
        objects[mapping.get(HttpServletRequest.class.getSimpleName())] = req;
        // 放响应对象
        objects[mapping.get(HttpServletResponse.class.getSimpleName())] = resp;
        // 执行请求
        try {
            Object invoke = handler.getMethod().invoke(handler.getControlObject(), objects);
            if (invoke!=null){
                resp.getWriter().write(invoke.toString());
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     *进行权限校验
     * @param handler
     * @param parameterMap
     * @return
     */
    private boolean canVisit(Handler handler, Map<String, String[]> parameterMap) {
        // 权限对象
        Visit visit = handler.getVisit();
        // 不需要校验
        if (visit==null) {
            return true;
        }
        // 校验值不存在!
        if (parameterMap.get(visit.getName())==null) {
            return false;
        }
        // 思路 待校验中有一个匹配上就返回true
        // 待校验的
        HashSet<String> visitSet=new HashSet<>(Arrays.asList(parameterMap.get(visit.getName())));
        // 有权限的
        Set<String> visitList = new HashSet<>(Arrays.asList(visit.getVisitArray()));
        List<String> collect = visitSet.stream().filter(n -> !visitList.add(n)).collect(Collectors.toList());
        // 没匹配上
        if (collect.isEmpty())  {
            return false;
        }
        return true;
    }

    /**
     * 根据请求路径获取处理器
     * @param requestURI
     * @return
     */
    private Handler getHandler(String requestURI) {
        for (Handler handler : handlerList) {
            if (handler.getPattern().matcher(requestURI).matches()) {
                return handler;
            }
        }
        return null;
    }
}

封装的Handler请求处理对象

package com.my.test.pojo;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * 请求处理器
 * @author HuYuanLiang
 * @since 2021/5/29
 */
public class Handler {
    /**
     * 匹配的url
     */
    private Pattern pattern;
    /**
     * / 参数顺序,是为了进⾏参数
     * 绑定,key是参数名,value代表是第⼏个参数 <name,2>
     */
    private Map<String,Integer> paramIndexMapping;
    /**
     * 反射方法对象
     */
    private Method method;
    /**
     * controlObject 对象
     */
    private Object controlObject;

    /**
     * 能访问的权限对象
     */
    private Visit visit;

    public Visit getVisit() {
        return visit;
    }

    public void setVisit(Visit visit) {
        this.visit = visit;
    }

    public Pattern getPattern() {
        return pattern;
    }

    public void setPattern(Pattern pattern) {
        this.pattern = pattern;
    }

    public Map<String, Integer> getParamIndexMapping() {
        return paramIndexMapping;
    }

    public void setParamIndexMapping(Map<String, Integer> paramIndexMapping) {
        this.paramIndexMapping = paramIndexMapping;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object getControlObject() {
        return controlObject;
    }

    public void setControlObject(Object controlObject) {
        this.controlObject = controlObject;
    }
}

封装的Visit权限对象

package com.my.test.pojo;

/**
 * @author Administrator
 */
public class Visit {

    public Visit() {
    }

    public Visit(String name, String[] visitArray) {
        this.name = name;
        this.visitArray = visitArray;
    }

    /**
     * 需要检验的key
     */
    private String name;

    /**
     * 能访问的权限 用户名数组
     */
    private String[] visitArray;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String[] getVisitArray() {
        return visitArray;
    }

    public void setVisitArray(String[] visitArray) {
        this.visitArray = visitArray;
    }
}

源码地址

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

起风了 收衣服

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

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

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

打赏作者

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

抵扣说明:

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

余额充值