手撸Spring系列7:Spring MVC(实战篇)

说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。

手撸Spring系列是笔者本人首次尝试的、较为规范的系列博客,将会围绕Spring框架分为 IOC/DI 思想Spring MVCAOP 思想Spring JDBC 四个模块,并且每个模块都会分为 理论篇源码篇实战篇 三个篇章进行讲解(大约12篇文章左右的篇幅)。从原理出发,深入浅出,一步步接触Spring源码并手把手带领大家一起写一个 迷你版的Spring框架 ,促进大家进一步了解Spring的本质!

由于源码篇涉及到源码的阅读,可能有小伙伴没有成功构建好Spring源码的阅读环境,笔者强烈建议:想要真正了解Spring,一定要构建好源码的阅读环境再进行研究,具体构建过程可查看笔者此前的博客:《如何构建Spring5源码阅读环境》

前言

经过前面Spring MVC 理论篇和源码篇的讲解,我们终于又再次迎来了实战篇。( 注意:Spring MVC 实战篇代码的运行建立在Spring IOC 实战篇中的代码基础之上!!,在阅读此篇博客之前,建议先将前面章节的博客浏览一遍~!!

与Spring IOC 的实战篇源码一样,笔者也会开源到码云Gitee上,仓库地址如下:https://gitee.com/bosen-once/mini-spring

在正式编写代码之前,还是让各位读者朋友们先看一下整个程序的大致架构(如下)


一、编写前准备

由于我们是编写与web相关的框架代码,因此不能少了我们熟悉的老朋友Servlet的加入(需要导入第三方的包),依赖如下:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>

二、注解类的编写

和Spring IOC 实战篇一样,我们的编写工作还是先从最简单的注解类开始,Spring MVC 的注解类在IOC注解的基础上还要加上两个注解类(@RequestMapping@RequestParam

1.@RequestMapping

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>url映射注解</p>
 * @author Bosen
 * @date 2021/9/15 22:06
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

2.@RequestParam

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>参数注解</p>
 * @author Bosen
 * @date 2021/9/15 22:43
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";
}

三、核心类的编写

在Spring MVC 理论篇和源码篇中,我们得知了在Spring MVC 的核心类主要是前端控制器DispatcherServlet、映射器HandlerMapping、适配器HandlerAdapter、视图解析器ViewResolver、视图View以及ModelAndView,那么我们的编写任务就是完成上述几个类迷你版的实现!!

1.HandlerMapping

知识温故: HandlerMapping是用于保存URL和Method的对应关系的映射器。

package org.springframework.web.servlet;

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

/**
 * <p>用于保存URL和Method的对应关系</p>
 * @author Bosen
 * @date 2021/9/15 11:24
 */
public class HandlerMapping {
    /**
     * <p>对应的controller对象</p>
     */
    private Object controller;

    /**
     * <p>对应的方法</p>
     */
    private Method method;

    /**
     * <p>URL的封装</p>
     */
    private Pattern pattern;

    public HandlerMapping(Object controller, Method method, Pattern pattern) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }

    public Method getMethod() {
        return method;
    }

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

    public Pattern getPattern() {
        return pattern;
    }

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

2.HandlerAdapter

知识温故: HandlerAdapter是完成参数列表与Method实参的对应关系的适配器。

package org.springframework.web.servlet;

import org.springframework.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>完成参数列表与Method实参的对应关系</p>
 * @author Bosen
 * @date 2021/9/15 13:04
 */
public class HandlerAdapter {
    /**
     * <p>判断handler是否属于HandlerMapping</p>
     */
    public boolean supports(Object handler) {
        return handler instanceof HandlerMapping;
    }

    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws InvocationTargetException, IllegalAccessException {
        HandlerMapping handlerMapping = (HandlerMapping) handler;

        // 方法的形参列表
        Map<String, Integer> paramMapping = new HashMap<>();

        // 处理被@ReauestParam标记的参数
        Annotation[][] paramAnnotations = handlerMapping.getMethod().getParameterAnnotations();
        for (int i = 0; i < paramAnnotations.length; i++) {
            for (Annotation paramAnnotation : paramAnnotations[i]) {
                if (paramAnnotation instanceof RequestParam) {
                    String paramName = ((RequestParam) paramAnnotation).value();
                    if (!"".equals(paramName.trim())) {
                        paramMapping.put(paramName, i);
                    }
                }
            }
        }

        // 处理HttpServletRequest和HttpServletResponse参数
        Class<?>[] paramTypes = handlerMapping.getMethod().getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            Class<?> type = paramTypes[i];
            if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
                paramMapping.put(type.getName(), i);
            }
        }

        // 用户通过URL传递过来的参数列表
        Map<String, String[]> requestParamMap = request.getParameterMap();

        // 实参列表
        Object[] paramValues = new Object[paramTypes.length];

        // 设置由@RequestParam标记的参数
        for (Map.Entry<String, String[]> param : requestParamMap.entrySet()) {
            String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "");
            // 如果方法的形参列表中不存在该参数则跳过
            if (!paramMapping.containsKey(param.getKey())) {
                continue;
            }
            // 获取该参数在形参列表中的下标
            int index = paramMapping.get(param.getKey());
            // 参数类型转换
            paramValues[index] = caseStringValue(value, paramTypes[index]);
        }

        // 设置request参数
        if (paramMapping.containsKey(HttpServletRequest.class.getName())) {
            paramValues[paramMapping.get(HttpServletRequest.class.getName())] = request;
        }

        // 设置response参数
        if (paramMapping.containsKey(HttpServletResponse.class.getName())) {
            paramValues[paramMapping.get(HttpServletResponse.class.getName())] = response;
        }

        // 执行目标方法
        Object result = handlerMapping.getMethod().invoke(handlerMapping.getController());

        if (handlerMapping.getMethod().getReturnType() == ModelAndView.class) {
            return (ModelAndView) result;
        }

        return null;
    }

    /**
     * <p>参数类型的转换</p>
     */
    private Object caseStringValue(String value, Class<?> clazz) {
        if (clazz == String.class) {
            return value;
        }
        if (clazz == Integer.class || clazz == int.class) {
            return Integer.valueOf(value);
        }
        return null;
    }
}

3.ModelAndView

知识温故: ModelAndView用于存储视图名称和数据

package org.springframework.web.servlet;

import java.util.Map;

/**
 * <p>存储视图名称和数据</p>
 * @author Bosen
 * @date 2021/9/15 13:04
 */
public class ModelAndView {
    /**
     * <p>视图名称</p>
     */
    private String viewName;

    /**
     * <p>参数</p>
     */
    private Map<String, ?> model;

    public ModelAndView(String viewName) {
        this.viewName = viewName;
    }

    public ModelAndView(String viewName, Map<String, ?> model) {
        this.viewName = viewName;
        this.model = model;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, ?> getModel() {
        return model;
    }

    public void setModel(Map<String, ?> model) {
        this.model = model;
    }
}

4.ViewResolver

知识温故: ViewResolver完成模板名称和模板解析引擎的匹配的工作

package org.springframework.web.servlet;

import java.io.File;

/**
 * <p>模板名称和模板解析引擎的匹配</p>
 * @author Bosen
 * @date 2021/9/15 13:06
 */
public class ViewResolver {
    /**
     * <p>默认解析的文件后缀</p>
     */
    private final String DEFAULT_TEMPLATE_SUFFIX = ".html";

    /**
     * <p>模板文件的目录</p>
     */
    private File templateRootDir;

    /**
     * <p>视图名称</p>
     */
    private String viewName;

    public ViewResolver(String templateRoot) {
        String templateRootPath = this.getClass().getResource(templateRoot).getFile();
        this.templateRootDir = new File(templateRootPath);
    }

    /**
     * <p>解析视图名</p>
     */
    public View resolveViewName(String viewName) {
        this.viewName = viewName;
        if (viewName == null || "".equals(viewName.trim())) {
            return null;
        }
        // 如果viewName没有加上后缀则加上
        viewName = viewName.endsWith(DEFAULT_TEMPLATE_SUFFIX) ? viewName : viewName + DEFAULT_TEMPLATE_SUFFIX;
        // 获取对应的模板文件
        File templateFile = new File((templateRootDir.getPath() + "/" + viewName).replaceAll("/+", "/"));
        // 通过模板文件返回视图对象
        return new View(templateFile);
    }
}

5.View

知识温故: View完成视图的渲染(注入model)并将视图响应给用户

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>视图对象</p>
 * @author Bosen
 * @date 2021/9/15 13:11
 */
public class View {
    /**
     * <p>内容类型</p>
     */
    public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=utf-8";

    /**
     * <p>模板文件</p>
     */
    private File viewFile;

    public View(File viewFile) {
        this.viewFile = viewFile;
    }

    /**
     * <p>解析模板文件,将model注入到view中,并respone给用户</p>
     */
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws IOException {
        RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
        StringBuffer sb = new StringBuffer();

        // 对模板文件逐行读取,当有符合${}包裹的变量时,将其替换掉
        try {
            String line = null;
            while ((line = ra.readLine()) != null) {
                // 避免出现乱码
                line = new String(line.getBytes("ISO-8859-1"), "utf-8");
                // 利用正则查找每一行中由${}包裹的变量
                Pattern pattern = Pattern.compile("\\$\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
                Matcher matcher = pattern.matcher(line);

                while (matcher.find()) {
                    // 获取变量名
                    String paramName = matcher.group();
                    paramName = paramName.replaceAll("\\$\\{|\\}", "");
                    // 查找model中对应的变量
                    Object paramValue = model.get(paramName);
                    if (paramValue == null) {
                        continue;
                    }
                    line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
                    matcher = pattern.matcher(line);
                }
                sb.append(line);
            }
        } finally {
            ra.close();
        }
        // 通过response响应给用户
        response.setContentType(DEFAULT_CONTENT_TYPE);
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(sb.toString());
    }

    /**
     * <p>处理特殊字符串</p>
     */
    public static String makeStringForRegExp(String string) {
        return string
                .replace("\\", "\\\\")
                .replace("*", "\\")
                .replace("+", "\\+")
                .replace("|", "\\|")
                .replace("{", "\\{")
                .replace("}", "\\}")
                .replace("(", "\\(")
                .replace(")", "\\)")
                .replace("^", "\\^")
                .replace("$", "\\$")
                .replace("[", "\\[")
                .replace("]", "\\]")
                .replace("?", "\\?")
                .replace(",", "\\,")
                .replace(".", "\\.")
                .replace("&", "\\&");
    }
}

6.DispatcherServlet

知识温故: DispatcherServlet主要完成Spring MVC 的初始化工作,以及与用户的交互,通过它来调用映射器、适配器等组件工作。

package org.springframework.web.servlet;

import org.springframework.annotation.Controller;
import org.springframework.annotation.RequestMapping;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

import javax.servlet.ServletConfig;
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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>Spring MVC 入口类</p>
 * @author Bosen
 * @date 2021/9/15 11:19
 */
public class DispatcherServlet extends HttpServlet {

    private final String LOCATION = "contextConfigLocation";

    private List<HandlerMapping> handlerMappings = new ArrayList<>();

    private Map<HandlerMapping, HandlerAdapter> handlerAdapters = new HashMap<>();

    private List<ViewResolver> viewResolvers = new ArrayList<>();

    private AbstractApplicationContext context;

    @Override
    public void init(ServletConfig config) {
        try {
            // 初始化IOC容器
            context = new AnnotationConfigApplicationContext(
                    Class.forName(config.getInitParameter(LOCATION)));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 初始化Spring MVC九大组件
        initStrategies(context);
    }

    /**
     * <p>初始化Spring MVC九大组件</p>
     * @param context IOC容器
     */
    protected void initStrategies(AbstractApplicationContext context) {
        // 初始化多文件上传组件
        initMultipartResolver(context);
        // 初始化本地语言环境
        initLocaleResolver(context);
        // 初始化模板处理器
        initThemeResolver(context);
        // 初始化映射器
        initHandlerMappings(context);
        // 初始化适配器
        initHandlerAdapters(context);
        // 初始化异常拦截器
        initHandlerExceptionResolvers(context);
        // 初始化视图预处理器
        initRequestToViewNameTranslator(context);
        // 初始化视图转换器
        initViewResolvers(context);
        // 初始化FlashMap管理器
        initFlashMapManager(context);
    }

    /**
     * <p>初始化九大组件的方法,由于我们实现的是迷你版的spring,只对映射器、视图解析器和适配器做初始化</p>
     */
    private void initMultipartResolver(AbstractApplicationContext context) {}
    private void initLocaleResolver(AbstractApplicationContext context) {}
    private void initThemeResolver(AbstractApplicationContext context) {}
    private void initHandlerExceptionResolvers(AbstractApplicationContext context) {}
    private void initRequestToViewNameTranslator(AbstractApplicationContext context) {}
    private void initFlashMapManager(AbstractApplicationContext context) {}

    /**
     * <p>初始化映射器</p>
     * @param context IOC容器
     */
    private void initHandlerMappings(AbstractApplicationContext context) {

        // 从IOC容器中获取所有的实例
        List<String> beanNames = new ArrayList<>(context.beanDefinitionMap.keySet());

        for (String beanName : beanNames) {
            // 获取当前bean的类型
            Object controller = context.getBean(beanName);
            Class<?> clazz = controller.getClass();

            // 如果没有被@Controller标记则跳过
            if (!clazz.isAnnotationPresent(Controller.class)) {
                continue;
            }

            // 如果该类中被@RequestMapping标记,则需要获取请求地址的前缀
            String baseUrl = "";
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
                baseUrl = requestMapping.value();
            }

            // 扫描所有public类型的方法
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                // 如果该方法没有被@RequestMapping标记则跳过
                if (!method.isAnnotationPresent(RequestMapping.class)) {
                    continue;
                }
                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                String regex = "/" + baseUrl + requestMapping.value()
                        .replaceAll("\\*", "*")
                        .replaceAll("/+", "+");
                Pattern pattern = Pattern.compile(regex);
                this.handlerMappings.add(new HandlerMapping(controller, method, pattern));
            }
        }
    }
    
    /**
     * <p>初始化适配器</p>
     * @param context IOC容器
     */
    private void initHandlerAdapters(AbstractApplicationContext context) {
        for (HandlerMapping handlerMapping : handlerMappings) {
            this.handlerAdapters.put(handlerMapping, new HandlerAdapter());
        }
    }

    /**
     * <p>初始化视图解析器</p>
     * @param context IOC容器
     */
    private void initViewResolvers(AbstractApplicationContext context) {
        // 默认扫描"/template"目录
        String templateRoot = "/templates";
        String templateRootPath = this.getClass().getResource(templateRoot).getFile();

        File templateRootDir = new File(templateRootPath);

        Arrays.asList(templateRootDir.listFiles()).forEach(
                iter -> this.viewResolvers.add(new ViewResolver(templateRoot))
        );
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            doDispatch(request, response);
        } catch (Exception e) {
            response.getWriter().write("" +
                    "<h1>500</h1><hr/>" + Arrays.toString(e.getStackTrace()));
            e.printStackTrace();
        }
    }

    private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws IOException, InvocationTargetException, IllegalAccessException {
        // 根据用户请求获取映射器
        HandlerMapping handler = getHandler(request);
        if (handler == null) {
            processDispatchResult(request, response, new ModelAndView("404"));
            return;
        }
        // 获取适配器
        HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
        // 调用对应的方法
        ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
        // 渲染并响应给用户
        processDispatchResult(request, response, modelAndView);
    }

    /**
     * <p>获取映射器</p>
     */
    private HandlerMapping getHandler(HttpServletRequest request) {
        if (this.handlerMappings.isEmpty()) {
            return null;
        }

        String url = request.getRequestURI();

        for (HandlerMapping handlerMapping : this.handlerMappings) {
            Matcher matcher = handlerMapping.getPattern().matcher(url);
            if (matcher.matches()) {
                return handlerMapping;
            }
        }
        return null;
    }

    /**
     * <p>获取适配器</p>
     */
    private HandlerAdapter getHandlerAdapter(HandlerMapping handlerMapping) {
        if (this.handlerAdapters.isEmpty()) {
            return null;
        }
        // 获取映射器相对应的适配器
        HandlerAdapter handlerAdapter = this.handlerAdapters.get(handlerMapping);
        if (handlerAdapter.supports(handlerMapping)) {
            return handlerAdapter;
        }
        return null;
    }

    /**
     * <p>渲染并响应给用户</p>
     */
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) throws IOException {
        if (modelAndView == null || this.viewResolvers.isEmpty()) {
            return;
        }
        for (ViewResolver viewResolver : viewResolvers) {
            View view = viewResolver.resolveViewName(modelAndView.getViewName());
            if (view != null) {
                view.render(modelAndView.getModel(), request, response);
                return;
            }
        }
    }
}

至此,迷你版的Spring MVC 的已经通过我们灵巧的双手撸出来啦!!接下来对这个刚刚完成的迷你版框架进行测试!


四、迷你版Spring MVC测试

1.ApplicationConfig

编写一个配置类

package org.springframework.test.config;

import org.springframework.annotation.ComponentScan;

/**
 * <p>配置类</p>
 * @author Bosen
 * @date 2021/9/11 14:11
 */
@ComponentScan("org.springframework.test")
public class ApplicationConfig {}

2.TestController

编写一个控制器

package org.springframework.test.controller;

import org.springframework.annotation.Controller;
import org.springframework.annotation.RequestMapping;
import org.springframework.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Bosen
 * @date 2021/9/11 22:31
 */
@Controller
public class TestController {
    @RequestMapping("/test")
    public ModelAndView test(@RequestParam("author") String author) {
        Map<String, Object> model = new HashMap<>();
        model.put("author", author);
        return new ModelAndView("test", model);
    }

    @RequestMapping("/exception")
    public void exception() {
        int i = 1/0;
    }
}

3.test.html

TestController.test中指定跳转的页面(即:测试页面),${author}经过View对象的渲染后会替换成具体的值。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
@author: ${author}
</body>
</html>

4.web.xml

首先先来配置web项目的配置文件,主要将前端控制器DispatcherServlet、和配置类ApplicationConfig的全类路径填写正确即可!

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>mini-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>org.springframework.test.config.ApplicationConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mini-mvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

5.404.html

当用户输入了不存在的url时要跳转的页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>
<h1>404</h1>
</body>
</html>

6.开始测试

启动web程序后,首先让我们先测试404页面是否能显示成功,访问一个映射器中不存在的地址,如:http://localhost/demo(笔者的tomcat端口设置了为80),进入该地址,页面显示如下:

接着测试出现异常的页面,访问:http://localhost/exception (该地址故意执行了1/0的逻辑错误代码)页面显示如下:

在404和500页面都测试无误后,我们开始测试具体要给用户展示的页面,即需要View对象渲染并响应的页面,访问:http://localhost/test?author=Bosen 页面显示如下:

至此,我们迷你版的Spring MVC 已经成功实现并测试完毕~!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云丶言

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

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

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

打赏作者

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

抵扣说明:

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

余额充值