Spring MVC源码分析与实践

基础知识

一、网站架构及其演变过程

三大模型架构

  1. 单体应用
    • 画图板、记事本
  2. C/S架构
    • 灵活性和安全性较高,如QQ、WPS
    • 业务分别拆分到客户端(安全性和稳定性较差)和服务端(安全性/稳定性较好且易升级,但会增大服务器的负担)实现
  3. B/S架构
    • 服务端又可以拆分成服务端程序和应用端程序
      1. 服务端程序负责统一处理数据连接、封装、解析等工作,如Tomcat服务器
      2. 应用端程序专注与业务处理逻辑以及与数据端的交互
    • 相比C/S架构,开发得到简化

浅谈B/S架构

  • TCP/IP模型
    1. 四层架构模型(接入层<对应OSI的物理层和链路层>、网络层、传输层、应用层)
    2. 网络层–>IP协议;传输层–>TCP协议;应用层–>HTTP协议
  • OSI标准参考模型
    1. 七层模型(物链网输会式应)
  • 常用协议补充
    1. 网络层:IPV4、IPV6…以及常见路由协议
    2. 传输层:TCP、UDP(直播等可靠性要求不高的应用场景下使用)
    3. 应用层:DNS、FTP、SMTP、POP…
  • B/S架构的演变过程(海量数据引起的变革)
    1. 缓存
      • 降低数据库的访问压力
      • 根据场景建立空缓存也是一种有效的方法
      • 缓存失效机制(定期失效…)
      • 程序实现:Map–>ConcurrentHashMap
      • 框架实现:Redis、Memcache…
    2. 页面静态化
      • 保存生成的页面模板,节省生成页面的资源
    3. 数据库优化
      • 表结构优化
      • SQL语句优化(语法层面、业务逻辑层面、配合索引和缓存)
      • 分区
      • 分表
      • 索引优化
      • 使用存储过程代替程序操作
    4. 分离活跃数据(分表)
    5. 批量读取
      • 合并查询
      • IN 语句
    6. 延迟修改
    7. 读写分离
      • 搭建数据库服务器集群
      • 主数据库接受写请求,并承担同步到从数据库的任务
      • 从数据库承担读请求
    8. 分布式数据库
    9. NoSQL和Hadoop
  • 高并发解决方案
    1. 应用和静态资源分离
    2. 页面缓存
    3. 集群和分布式
      • Session同步
      • 事务处理
      • 节点依赖
      • 请求分发
    4. 反向代理
      • 代理服务器(不透明、不需要域名)
      • 反向代理服务器(透明、需要域名)
    5. CDN
      • 全国节点搭建
      • 二级DNS分配访问从缓存服务器
      • 从缓存服务器没有资源则访问主服务器
      • 如各类通信运营商的节点访问方案
    6. 底层优化

二、常见协议和标准

DNS

  • DNS服务器(多级结构:根->权限->多级)
  • hosts文件(比本地域名服务器优先级还高的解析)
C:\windows\system32\drivers\etc\hosts
nslookup www.baidu.com

TCP/IP

  • IP协议
    • 负责路由选择
  • TCP协议
    • 负责可靠传输
    • 三次握手
    • 四次挥手
    • SYN泛洪攻击(客户端利用不发送第三次握手使服务器重发第二次握手,消耗服务器资源)
    • 通常应用于网页、邮件等服务
  • UDP协议
    • 传输速率更快,不需要建立连接
    • 应用于视频、语音传输

HTTP

  • 请求报文和响应报文
  • 首行、头部、主体
    • 请求方法(GET、HEAD、POST、DELETE)
    • 响应状态码
      1. 1XX(信息性状态码)
      2. 2XX(成功状态码)
      3. 3XX(重定向状态码)
      4. 4XX(客户端错误)
      5. 5XX (服务端错误)

Servlet

  • 对HTTP接收到的数据处理并产生要返回给客户端的结果

MVC基本概念

  • Model:模型层(处理数据)
    1. 实体类Beans,存储业务数据
    2. 业务处理Beans(Service,Dao),实现业务逻辑和数据访问
  • View:视图层,与用户交互,展示数据
  • Controller:Servlet,接收请求和响应浏览器
  • 补充三层架构:表示层、业务逻辑层、数据访问层

MVC工作流程

  1. 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收
  2. Controller调用相应的Model层处理请求,处理完毕将结果返回到Controller
  3. Controller再根据请求处理的结果
    找到相应的View视图,渲染数据后最终响应给浏览器

三、Java Socket

Socket用法

  • ServerSocket创建一个服务器监听对象,并使用.accept()返回一个Socket对象进行通信
  • Socket对象进行通信,可以通过.get()获取输入输出流
  • 通过Reader和Writer对流进行读写
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) {
        try{
            //创建8080监听端口
            ServerSocket server = new ServerSocket(8080);
            //等待请求
            Socket socket = server.accept();
            //接收到请求后使用Socket通信
            //使用BufferedReader读取数据
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //等待缓冲区数据写入flush()
            String line = is.readLine();
            System.out.println("received from client: "+line);

            //创建PrintWriter用于发送数据
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            pw.println("received data: "+line);
            pw.flush();

            //关闭资源
            pw.close();
            is.close();
            socket.close();
            server.close();
        }catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {
    public static void main(String[] args) {
        String msg = "Client Data.";
        try {
            //创建一个Socket,跟本机的8080端口通信
            Socket socket = new Socket("127.0.0.1",8080);
            //使用Socket创建PrintWriter和BufferedReader进行读写数据
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //发送数据
            pw.println(msg);
            //数据冲入缓冲区,此时服务器才接收到数据流
            pw.flush();
            //接收数据
            String line = is.readLine();
            System.out.println("received from server: "+line);
            //关闭资源
            pw.close();
            is.close();
            socket.close();
        }catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

NioSocket用法

  • Nio(New IO)是JDK1.4之后重写的更高效的IO
  • 提供ServerSocketChannel与SocketChannel分别对应于原来的ServerSocket和Socket
    1. Buffer:担任货物的角色
    2. Channel:担任送货员的角色
    3. Selector:担任分拣员的角色
      • Selection.Key.OP_ACCEPT
      • Selection.Key.OP_CONNECT
      • Selection.Key.OP_READ
      • Selection.Key.OP_WRITE
    4. SelectionKey
      • 保存处理当前请求的Channel和Selector
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

/**
 * @author fancm
 */

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel
        //此处采用静态类的open工厂方法获取ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //获取socket且绑定8080
        ssc.socket().bind(new InetSocketAddress(8080));
        //阻塞模式:缓冲区没有数据会阻塞线程
        //非阻塞模式:缓冲区没有数据会立即返回
        //设置为非阻塞模式,阻塞模式下不可注册选择器
        ssc.configureBlocking(false);
        //为ssc注册选择器,同样使用静态工厂方法
        Selector selector = Selector.open();
        //设置接收请求状态
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        //创建处理器
        Handler handler = new Handler(1024);
        while(true)
        {
            //等待请求,每次等待阻塞3s,超过3s后线程继续相信运行
            //如果传入0或者不传参将一直阻塞
            if(selector.select(3000)==0){
                System.out.println("等待请求超时...");
                continue;
            }
            System.out.println("处理请求...");
            //获取待处理的SelectionKey
            //分拣员selector接收所有请求,并根据类型进行分拣
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
            while(keyIter.hasNext()){
                SelectionKey key = keyIter.next();
                try {
                    //接收到连接请求时
                    if(key.isAcceptable()){
                        handler.handleAccept(key);
                    }
                    //读数据
                    if(key.isReadable())
                    {
                        handler.handlerRead(key);
                    }
                }catch (IOException e){
                    keyIter.remove();
                    continue;
                }
                //处理完后,从待处理的keyIter中移除当前使用的key
                keyIter.remove();
            }
        }
    }

    //内部类实现处理
    private static class Handler {
        private int bufferSize = 1024;
        private String localCharset = "UTF-8";

        public Handler(){}
        public Handler(int bufferSize) {
            this(bufferSize,null);
        }
        public Handler(String localCharsSet)
        {
            this(-1,localCharsSet);
        }
        public Handler(int bufferSize,String localCharset)
        {
            if(bufferSize>0)
                this.bufferSize = bufferSize;
            if(localCharset!=null)
                this.localCharset = localCharset;
        }

        public void handleAccept(SelectionKey key) throws IOException{
            //SelectionKey 保存Channel和Selector
            SocketChannel sc = ((ServerSocketChannel)key.channel()).accept();
            sc.configureBlocking(false);
            sc.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
            System.out.println("handleAccept process...");
        }

        public void handlerRead(SelectionKey key) throws IOException{
            //获取Channel
            SocketChannel sc = (SocketChannel)key.channel();
            //获取buffer并重置
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            //没有读取到内容则关闭
            if(sc.read(buffer)==-1){
                sc.close();
            }else{
                //将buffer转为读状态
                buffer.flip();
                //将buffer中接收到的值按localCharset格式编码后保存到receivedString
                String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                System.out.println("received from client: "+receivedString);

                //返回数据给客户端
                String sendString = "received data: "+receivedString;
                buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
                sc.write(buffer);
                //关闭Socket
                sc.close();
            }
            System.out.println("handleAccept process...");
        }
    }
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * @author fancm
 */

public class Client {
    public static void main(String[] args) {
        String msg = "Client Data.";
        try {
            //创建一个Socket,跟本机的8080端口通信
            //连接状态通信key.isAcceptable()==true
            Socket socket = new Socket("127.0.0.1",8080);
            //使用Socket创建PrintWriter和BufferedReader进行读写数据
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //发送数据
            pw.println(msg);
            //数据冲入缓冲区,此时服务器才接收到数据流
            //数据可读状态key.isReadable()==true
            pw.flush();
            //接收数据
            String line = is.readLine();
            System.out.println("received from server: "+line);
            //关闭资源
            pw.close();
            is.close();
            socket.close();
        }catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

源代码

一、详解Servlet

Servlet结构

  • ServletConfig
  • Servlet
    • GenericServlet
      • HttpServlet

框架层级(Application\Config\Context)?

Servlet接口

//容器启动时调用,只调用一次
void init(ServletConfig config) throws ServletException;
//获取ServletConfig对象
ServletConfig getServletConfig();
//处理一个请求
void service(ServletRequest req,ServletResponse res) throws ServletException,IOException;
//获取相关信息
String getServletInfo();
//销毁且释放资源,只调用一次
void destory();
  • ServletConfig配置
    • SpringMVC通过指定xml配置文件构造ServletConfig对象
    • Tomcat传入org.apache.catalina.core.StandardWrapper作为配置对象

GenericServlet

  • 是Servlet的默认实现
    1. 实现了ServletConfig接口
    2. 提供了无参的init方法
    3. 提供了log方法
//ServletConfig接口
ServletContext getServletConfig().getServletContext();
//GenericServlet接口
ServletContext getServletContext();
//日志接口
getServletContext().log(String msg);

HttpServlet(具体实现)

  • 是用Http协议实现的Servlet的基类,开发中写Servlet直接继承HttpServlet就可以了
  • 主要重写了service(),重载的Servlet参数为新的HttpServlet参数,并根据Http请求类型路由到不同的处理方法
  • 对doGet()方法进行过期校验
/**
*from javax.jar
*/
//先转换ServletRequest -->HttpServletRequest 以及ServletResponse -->HttpServletResponse 
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
            this.service(request, response);
        } else {
            throw new ServletException("non-HTTP request or response");
        }
    }

//根据Http请求类型路由到不同的请求方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader("If-Modified-Since");
                if (ifModifiedSince < lastModified) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);	//过期校验
                }
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }
//doGet()
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_get_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);	//方法错误
        } else {
            resp.sendError(400, msg);	//非法请求
        }

    }

二、Tomcat分析(暂时跳过)

整体结构

  • Server:顶层容器代表整个服务器(只有1个)
  • Service:可以有多个,提供具体的服务
  • Connector:可以有多个,对应对同一服务的多个请求
  • Container:只能有一个,用于封装和管理Servlet
    Tomcat整体结构

工作方法(以下方法都是按容器的结构逐层调用)

  • load()
  • start()
  • stop()
  • await()

Bootstrap启动过程

实践

一、环境搭建

  1. 创建Maven项目
  2. 配置pom.xml
<!--2-->
<!-- 配置Maven打包方式 -->
<packaging>war</packaging>
<!-- 导入必要依赖 -->
<dependencies>
        <!-- SpringMVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.1</version>
        </dependency>
        <!-- 日志 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!-- ServletAPI -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <!-- 不会被打进War包(已被服务器提供) -->
            <scope>provided</scope>
        </dependency>
        <!-- Spring5和Thymeleaf整合包 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.12.RELEASE</version>
        </dependency>
    </dependencies>
  1. 构建Web项目结构
    3
  2. 配置web.xml
    • SpringMVC的配置文件默认位于WEB-INF下,默认名称[servlet-name]-servlet.xml
    • 可以通过【init-param】标签指定SpringMvc的位置,classpath:表示\main
    • 注意web项目结构的根目录
<?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">
    <!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 -->
        <init-param>
            <!-- contextConfigLocation为固定值 -->
            <param-name>contextConfigLocation</param-name>
            <!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的
            src/main/resources -->
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!--
        作为框架的核心组件,在启动过程中有大量的初始化操作要做
        而这些操作放在第一次请求时才执行会严重影响访问速度
        因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
        -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
        -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  1. 创建控制器
package com.fancm.controller;
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
}
  1. 修改springMVC配置文件
    • 扫描Controller
    • 配置视图解析器
<?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">
    <!--扫描控制层组件-->
    <context:component-scan base-package="com.fancm.controller"/>

    <!-- 配置Thymeleaf视图解析器 -->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
        <property name="order" value="1"/>
        <property name="characterEncoding" value="UTF-8"/>
        <property name="templateEngine">
            <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
                <property name="templateResolver">
                    <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
                        <!-- 视图前缀 -->
                        <property name="prefix" value="/WEB-INF/templates/"/>
                        <!-- 视图后缀 -->
                        <property name="suffix" value=".html"/>
                        <property name="templateMode" value="HTML5"/>
                        <property name="characterEncoding" value="UTF-8" />
                    </bean>
                </property>
            </bean>
        </property>
    </bean>
</beans>
  1. 配置Tomcat
    • 从官网下载符合系统的Tomcat二进制文件
    • IDEA设置Tomcat启动并选择部署环境
    • 注意应用上下文:访问路径的URL根目录(MVC映射的所有URL请求都要在以该目录开头)
    • 启动Tomcat

环境搭建总结

  1. DispatcherServlet
    • 相当于SpringMVC的入口程序,通过这个Servlet对请求拦截和响应
    • 在web.xml中配置该Serlvet
      1. 控制层扫描
      2. 视图解析器
  2. 工作流程
    • 请求经过DispatcherServlet拦截后转发到响应的控制器
    • 控制器完成业务逻辑后返回视图名称
    • 视图解析器封装视图路径
    • 渲染器根据视图路径渲染页面

二、如何使用SpringMVC

RequestMapping注解

  1. 功能
    • 作用就是将请求和处理请求的控制器方法关联起来,建立映射关系
  2. 注解位置
    • 标识一个类
    • 标识一个方法
package com.fancm.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
//该控制器下的所有方法请求都要加一层test开头
@RequestMapping("/test")
public class RequestMappingTestController {
    //http://localhost:8080/springmvc/test/testRequestMapping
    @RequestMapping("/testRequestMapping")
    public String testRequestMapping(){
        return "success";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>RequestMappingTest</title>
</head>
<body>
<h1>success</h1>
</body>
</html>
  1. Value属性
    • 字符串数组参数
    • 能传入多个请求地址,将多个地址的请求映射到同一方法
@Controller
//该控制器下的所有方法请求都要加一层test开头
public class RequestMappingTestController {
    @RequestMapping(value = {"/testRequestMapping","/test"})
    public String testRequestMapping(){
        return "success";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<a th:href="@{/testRequestMapping}">
    测试@RequestMapping的value属性-- >/testRequestMapping
</a>
<br>
<a th:href="@{/test}">测试@RequestMapping的value属性-->/test</a>
<br>
</body>
</html>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring MVC源码包括多个组件和类。其中,Tomcat在启动时会通知Spring初始化容器,加载bean的定义信息并初始化所有单例bean。然后,Spring MVC会遍历容器中的bean,获取每个controller中方法访问的URL,并将URL和Controller保存到一个Map中。这一过程是由HandlerMapping组件完成的,它是Spring MVC中负责URL到Controller映射的组件。此外,在Spring MVC源码中还有一个抽象类FrameworkServlet,它重写了初始化方法initServletBean(),可以在控制台或日志中打印初始化Servlet的名称以及初始化所需的时间。 以上是关于Spring MVC源码的一些重要信息,这些组件和类协同工作,实现了Spring MVC框架的核心功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Spring MVC源码分析](https://blog.csdn.net/qq_38826019/article/details/117877511)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [SpringMVC源码解析](https://blog.csdn.net/qq_35512802/article/details/120659719)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值