Spring基础-Spring和Spring框架组成

前言

Spring是什么?它是怎么诞生的?有哪些主要的组件和核心功能呢? 本文通过这几个问题帮助你构筑Spring和Spring Framework的整体认知。

Spring简介

Spring 是分层的 full-stack(全栈) 轻量级开源框架,以 IoC 和 AOP 为内核,提供了展现层 Spring MVC 和业务层事务管理等众多的企业级应⽤技术,还能整合开源世界众多著名的第三⽅框架和类库,已经成为使⽤最多的 Java EE 企业应⽤开源框架。

Spring 官⽅⽹址:http://spring.io/

Spring 中文网址:https://springdoc.cn/spring/

我们经常说的 Spring 其实指的是Spring Framework(spring 框架)。 

Spring 发展历程  

  • 1997年 IBM 提出了EJB的思想;1998年,SUN 制定开发标准规范EJB1.0;1999年,EJB 1.1发 布;2001年,EJB 2.0发布;2003年,EJB 2.1发布;2006年,EJB 3.0发布;

  • Rod Johnson(spring之⽗)

    1. Expert One-to-One J2EE Design and Development(2002) 阐述了J2EE使⽤EJB开发设计的优点及解决⽅案

    2. Expert One-to-One J2EE Development without EJB(2004) 阐述了J2EE开发不使⽤EJB的解决⽅式(Spring雏形)

  • 2017 年 9 ⽉份发布了 Spring 的最新版本 Spring 5.0 通⽤版(GA)  

Spring优势

整个 Spring 优势,传达出⼀个信号,Spring 是⼀个综合性,且有很强的思想性框架,每学习⼀ 天,就能体会到它的⼀些优势。 

  • 方便解耦,简化开发

通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进⾏控制,避免硬编码所造成的 过度程序耦合。⽤户也不必再为单例模式类、属性⽂件解析等这些很底层的需求编写代码,可以更 专注于上层的应⽤。

  • AOP编程的⽀持

通过Spring的AOP功能,⽅便进⾏⾯向切⾯的编程,许多不容易⽤传统OOP实现的功能可以通过 AOP轻松应付。

  • 声明式事务的⽀持

@Transactional 可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式⽅式灵活的进⾏事务的管理,提⾼ 开发效率和质量。

  • 方便程序的测试

可以⽤⾮容器依赖的编程⽅式进⾏⼏乎所有的测试⼯作,测试不再是昂贵的操作,⽽是随⼿可做的 事情。

  • 方便集成各种优秀框架

Spring可以降低各种框架的使⽤难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、 Quartz等)的直接⽀持。

  • 降低JavaEE API的使⽤难度

Spring对JavaEE API(如JDBC、JavaMail、远程调⽤等)进⾏了薄薄的封装层,使这些API的使⽤ 难度⼤为降低。

  • 源码是经典的 Java 学习范例

Spring的源代码设计精妙、结构清晰、匠⼼独⽤,处处体现着⼤师对Java设计模式灵活运⽤以及对 Java技术的⾼深造诣。它的源代码无疑是Java技术的最佳实践的范例。 

Spring组件

图片

上图中包含了 Spring 框架的所有模块,这些模块可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。下面分别对这些模块的作用进行简单介绍(并且结合SpringFramework5.x源码模块帮助你对应好各模块关系)。

Core Container(Spring的核心容器)

容器是Spring框架最核⼼的部分,它管理着Spring应⽤中 bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了DI的功能。基于bean⼯⼚,我们还会发现有多种Spring应⽤上下⽂的实现。所有的Spring模块都构建于核心容器之上。 

  • Beans 模块:提供了框架的基础部分,包括控制反转和依赖注入。

  • Core 核心模块:封装了 Spring 框架的底层部分,包括资源访问、类型转换及一些常用工具类。

  • Context 上下文模块:建立在 Core 和 Beans 模块的基础之上,集成 Beans 模块功能并添加资源绑定、数据验证、国际化、Java EE 支持、容器生命周期、事件传播等。ApplicationContext 接口是上下文模块的焦点。

  • SpEL 模块:提供了强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从 Spring 容器获取 Bean,它也支持列表投影、选择和一般的列表聚合等。

对应的源码模块如下:

图片

Data Access/Integration(数据访问/集成)

数据访问/集成层包括 JDBC、ORM、OXM、JMS 和 Transactions 模块,具体介绍如下。

  • JDBC 模块:提供了一个 JDBC 的样例模板,使用这些模板能消除传统冗长的 JDBC 编码还有必需的事务控制,而且能享受到 Spring 管理事务的好处。

  • ORM 模块:提供与流行的“对象-关系”映射框架无缝集成的 API,包括 JPA、JDO、Hibernate 和 MyBatis 等。而且还可以使用 Spring 事务管理,无需额外控制事务。

  • OXM 模块:提供了一个支持 Object /XML 映射的抽象层实现,如 JAXB、Castor、XMLBeans、JiBX 和 XStream。将 Java 对象映射成 XML 数据,或者将XML 数据映射成 Java 对象。

  • JMS 模块:指 Java 消息服务,提供一套 “消息生产者、消息消费者”模板用于更加简单的使用 JMS,JMS 用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

  • Transactions 事务模块:支持编程和声明式事务管理。

对应的源码模块如下:

图片

Web模块

Spring 的 Web 层包括 Web、Servlet、WebSocket 和 Webflux 组件,具体介绍如下。

  • Web 模块:提供了基本的 Web 开发集成特性,例如多文件上传功能、使用的 Servlet 监听器的 IOC 容器初始化以及 Web 应用上下文。

  • Servlet 模块:提供了一个 Spring MVC Web 框架实现。Spring MVC 框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的 JSP 标签,完全无缝与 Spring 其他技术协作。

  • WebSocket 模块:提供了简单的接口,用户只要实现响应的接口就可以快速的搭建 WebSocket Server,从而实现双向通讯。

  • Webflux 模块:Spring WebFlux 是 Spring Framework 5.x中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。Spring WebFlux 用于创建基于事件循环执行模型的完全异步且非阻塞的应用程序。

此外Spring4.x中还有Portlet 模块,在Spring 5.x中已经移除

  • Portlet 模块:提供了在 Portlet 环境中使用 MVC 实现,类似 Web-Servlet 模块的功能。

对应的源码模块如下:

图片

AOP、Aspects、Instrumentation和Messaging

在 Core Container 之上是 AOP、Aspects 等模块,具体介绍如下:

  • AOP 模块:提供了面向切面编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态地把这些功能添加到需要的代码中,这样各司其职,降低业务逻辑和通用功能的耦合。

  • Aspects 模块:提供与 AspectJ 的集成,是一个功能强大且成熟的面向切面编程(AOP)框架。

  • Instrumentation 模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用。

  • messaging 模块:Spring 4.0 以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。

  • jcl 模块:Spring 5.x中新增了日志框架集成的模块。

对应的源码模块如下:

图片

Test模块

Test 模块:Spring 支持 Junit 和 TestNG 测试框架,而且还额外提供了一些基于 Spring 的测试功能,比如在测试 Web 框架时,模拟 Http 请求的功能。

包含Mock Objects, TestContext Framework, Spring MVC Test, WebTestClient。

对应的源码模块如下:

图片

Spring框架如何应用

上文中,我们展示了Spring和Spring Framework的组件, 这里对于开发者来说有几个问题:

  1. 首先,对于Spring进阶,直接去看IOC和AOP,存在一个断层,所以需要整体上构建对Spring框架认知上进一步深入,这样才能构建知识体系。

  2. 其次,很多开发者入门都是从Spring Boot开始的,他对Spring整体框架底层,以及发展历史不是很了解;特别是对于一些老旧项目维护和底层bug分析没有全局观。

  3. 再者,Spring代表的是一种框架设计理念,需要全局上理解Spring Framework组件是如何配合工作的,需要理解它设计的初衷和未来趋势。

如下是官方在解释Spring框架的常用场景的图

图片

我加上一些注释后,是比较好理解的;引入这个图,重要的原因是为后面设计一个案例帮助你构建认知。

设计一个Spring的Hello World

结合上面的使用场景,设计一个查询用户的案例的两个需求,来看Spring框架帮我们简化了什么开发工作:

  1. 查询用户数据 - 来看DAO+POJO-> Service 的初始化和装载。

  2. 给所有Service的查询方法记录日志

创建一个Maven的Java项目

图片

引入Spring框架的POM依赖

   <properties>        <maven.compiler.source>8</maven.compiler.source>        <maven.compiler.target>8</maven.compiler.target>        <spring.version>5.3.9</spring.version>        <aspectjweaver.version>1.9.6</aspectjweaver.version>    </properties>
    <dependencies>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-context</artifactId>            <version>${spring.version}</version>        </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-core</artifactId>            <version>${spring.version}</version>        </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-beans</artifactId>            <version>${spring.version}</version>        </dependency>        <dependency>            <groupId>org.aspectj</groupId>            <artifactId>aspectjweaver</artifactId>            <version>${aspectjweaver.version}</version>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>1.16.22</version>            <scope>compile</scope>        </dependency>        <dependency>            <groupId>junit</groupId>            <artifactId>junit</artifactId>            <version>4.12</version>            <scope>compile</scope>        </dependency>        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>fastjson</artifactId>            <version>1.2.25</version>            <scope>compile</scope>        </dependency>        <dependency>            <groupId>log4j</groupId>            <artifactId>log4j</artifactId>            <version>1.2.12</version>        </dependency>        <dependency>            <groupId>org.slf4j</groupId>            <artifactId>slf4j-api</artifactId>            <version>1.7.25</version>        </dependency>    </dependencies>

User对象​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:49 * @description */@Data@AllArgsConstructor@NoArgsConstructorpublic class User {
    /**     * user's name.     */    private String name;
    /**     * user's age.     */    private int age;
}

DAO 获取 POJO, UserDaoServiceImpl (mock 数据)​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:52 * @description */public class UserDaoImpl {
   public List<User> list(){       return Arrays.asList(new User("",18));   }}

增加daos.xml​​​​​​​

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="userDao" class="com.bcst.dao.xml.UserDaoImpl">        <!-- additional collaborators and configuration for this bean go here -->    </bean>    <!-- more bean definitions for data access objects go here -->
</beans>

业务层 UserService(调用DAO层)​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:52 * @description */public class UserService {
    private UserDaoImpl userDao;
    public void setUserDao(UserDaoImpl userDao) {        this.userDao = userDao;    }
    public List<User> list(){        return userDao.list();    }}

增加services.xml​​​​​​​

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="userService" class="com.bcst.service.xml.UserService">        <property name="userDao" ref="userDao"/>        <!-- additional collaborators and configuration for this bean go here -->    </bean>    <!-- more bean definitions for services go here --></beans>

拦截所有service中的方法,并输出记录​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 10:32 * @description */@Aspectpublic class LogAspect {
    @Around("execution(* com.bcst.service.*.*.*(..))")    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();        System.out.println("execute method:"+method.getName());        return joinPoint.proceed();    }}

增加aspects.xml

​​​​​​​

<?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:aop="http://www.springframework.org/schema/aop"       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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="com.bcst" />
    <aop:aspectj-autoproxy/>
    <bean id="logAspect" class="com.bcst.aspect.LogAspect">        <!-- configure properties of aspect here as normal -->    </bean>    <!-- more bean definitions for data access objects go here -->
</beans>

测试​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:58 * @description xml方式测试 */public class UserServiceTest {
    @Test    public void test1(){       // ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/daos.xml", "spring/services.xml");        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/daos.xml", "classpath:spring/services.xml","classpath:spring/aspects.xml");        UserService userService = applicationContext.getBean(UserService.class);        List<User> userList = userService.list();        System.out.println(JSON.toJSON(userList));    }}

Spring核心思想

上面的例子,体现了Spring IOC和AOP两个核心思想。

IOC和AOP不是spring提出的,在spring之前就已经存在,只不过更偏向于理论化,spring在技术层次把这两个思想做了⾮常好的实现(Java)  

控制反转-IOC

什么是IoC? 

IoC Inversion of Control (控制反转/反转控制),注意它是⼀个技术思想,不是⼀个技术实现。

描述的事情:Java开发领域对象的创建,管理的问题 。

传统开发方式:⽐如类A依赖于类B,往往会在类A中new⼀个B的对象 。

IoC思想下开发方式:我们不⽤⾃⼰去new对象了,⽽是由IoC容器(Spring框架)去帮助我们实例化对 象并且管理它,我们需要使⽤哪个对象,去问IoC容器要即可 。

我们丧失了⼀个权利(创建、管理对象的权利),得到了⼀个福利(不⽤考虑对象的创建、管理等⼀系列 事情)。

为什么叫做控制反转? 

控制:指的是对象创建(实例化、管理)的权利

反转:控制权交给外部环境了(spring框架、IoC容器)  

图片

IoC和DI的区别  

DI:Dependancy Injection(依赖注⼊)

怎么理解:IOC和DI描述的是同⼀件事情,只不过⻆度不⼀样罢了  

图片

案例分析

来看第一个需求:查询用户(service通过调用dao查询pojo),本质上如何创建User/Dao/Service等;

  • 如果没有Spring框架,我们需要自己创建User/Dao/Service等,比如:

UserDaoImpl userDao = new UserDaoImpl();UserService userService = new UserService();userService.setUserDao(userDao);List<User> userList = userService.list();
  • 有了Spring框架,可以将原有Bean的创建工作转给框架, 需要用时从Bean的容器中获取即可,这样便简化了开发工作

Bean的创建和使用分离了:​​​​​​​

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/daos.xml", "classpath:spring/services.xml","classpath:spring/aspects.xml");UserService userService = applicationContext.getBean(UserService.class);List<User> userList = userService.list();System.out.println(JSON.toJSON(userList));

图片

更进一步,你便能理解为何会有如下的知识点了

  1. Spring框架管理这些Bean的创建工作,即由用户管理Bean转变为框架管理Bean,这个就叫控制反转 - Inversion of Control (IoC)

  2. Spring 框架托管创建的Bean放在哪里呢?这便是IoC Container;

  3. Spring 框架为了更好让用户配置Bean,必然会引入不同方式来配置Bean?这便是xml配置,Java配置,注解配置等支持

  4. Spring 框架既然接管了Bean的生成,必然需要管理整个Bean的生命周期等;

  5. 应用程序代码从Ioc Container中获取依赖的Bean,注入到应用程序中,这个过程叫 依赖注入(Dependency Injection,DI) ;所以说控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式

  6. 在依赖注入时,有哪些方式呢?这就是构造器方式,@Autowired, @Resource, @Qualifier... 同时Bean之间存在依赖(可能存在先后顺序问题,以及循环依赖问题等)

这边引入我们后续的相关文章:Spring基础 - Spring之控制反转(IOC)

面向切面 - AOP

什么是AOP

AOP: Aspect oriented Programming ⾯向切⾯编程/⾯向⽅⾯编程

AOP是OOP的延续,从OOP说起

OOP三⼤特征:封装、继承和多态

OOP是⼀种垂直继承体系  。

图片

OOP编程思想可以解决⼤多数的代码重复问题,但是有⼀些情况是处理不了的,比如下⾯的在顶级⽗类 Animal中的多个⽅法中相同位置出现了重复代码,OOP就解决不了。

图片

横切逻辑代码 :

图片

横切逻辑代码存在什么问题:

  • 横切代码重复问题

  • 横切逻辑代码和业务代码混杂在⼀起,代码臃肿,维护不⽅便

AOP出场,AOP独辟蹊径提出横向抽取机制,将横切逻辑代码和业务逻辑代码分析  

图片

代码拆分容易,那么如何在不改变原有业务逻辑的情况下,悄⽆声息的把横切逻辑代码应⽤到原有的业 务逻辑中,达到和原来⼀样的效果,这个是⽐较难的 。

AOP在解决什么问题  

在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复 。

为什么叫做面向切面编程  

「切」:指的是横切逻辑,原有业务逻辑代码我们不能动,只能操作横切逻辑代码,所以⾯向横切逻辑 。

「⾯」:横切逻辑代码往往要影响的是很多个⽅法,每⼀个⽅法都如同⼀个点,多个点构成⾯,有⼀个 ⾯的概念在⾥⾯ 。

案例分析

来看第二个需求:给Service所有方法调用添加日志(调用方法时),本质上是解耦问题;

  • 如果没有Spring框架,我们需要在每个service的方法中都添加记录日志的方法,比如:

public List<User> list(){        System.out.println("execute method  list");        return userDao.list();}

  • 有了Spring框架,通过@Aspect注解 定义了切面,这个切面中定义了拦截所有service中的方法,并记录日志;可以明显看到,框架将日志记录和业务需求的代码解耦了,不再是侵入式的了​​​​​​​

@Around("execution(* com.bcst.service.*.*.*(..))")    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();        System.out.println("execute method:"+method.getName());        return joinPoint.proceed();    }

更进一步,你便能理解为何会有如下的知识点了

  • Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦,这个就叫面向切面编程 - Aspect Oriented Programming (AOP)

  • 为什么@Aspect注解使用的是aspectj的jar包呢?这就引出了Aspect4J和Spring AOP的历史渊源,只有理解了Aspect4J和Spring的渊源才能理解有些注解上的兼容设计

    如何支持更多拦截方式来实现解耦, 以满足更多场景需求呢?这就是@Around, @Pointcut... 等的设计

    那么Spring框架又是如何实现AOP的呢?这就引入代理技术,分静态代理和动态代理,动态代理又包含JDK代理和CGLIB代理

    这边引入我们后续的相关文章:Spring基础 - Spring之面向切面编程(AOP)

Spring框架设计如何逐步简化开发的

通过上述的框架介绍和例子,已经初步知道了Spring设计的两个大的要点:IOC和AOP;从框架的设计角度而言,更为重要的是简化开发,比如提供更为便捷的配置Bean的方式,直至0配置(即约定大于配置)。这里我将通过Spring历史版本的发展,来帮你理解Spring框架是如何逐步简化开发的。

Java配置方式改造

在上文的例子中, 通过xml配置方式实现的,这种方式实际上比较麻烦;我通过Java配置进行改造:

User,UserDaoImpl, UserService,LogAspect不用改,将原通过.xml配置转换为Java配置​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 11:06 * @description */@Configuration@EnableAspectJAutoProxypublic class BeanConfig {
    @Bean(value = "userDao")    public UserDaoImpl userDao(){        return new UserDaoImpl();    }
    @Bean(value = "userService")    public UserService userService(){        UserService userService = new UserService();        userService.setUserDao(userDao());        return userService;    }
    @Bean(value = "logAspect")    public LogAspect logAspect(){        return new LogAspect();    }
}

测试​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 11:10 * @description java配置方式改造 */public class UserServiceTest {
    @Test    public void test1(){        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);        UserService userService = applicationContext.getBean(UserService.class);        List<User> userList = userService.list();        System.out.println(JSON.toJSON(userList));    }}

注解配置方式改造

更进一步,Java 5开始提供注解支持,Spring 2.5 开始完全支持基于注解的配置并且也支持JSR250 注解。在Spring后续的版本发展倾向于通过注解和Java配置结合使用

BeanConfig 不再需要Java配置

/** * @description  * @author  * @date 2023/8/31 0031 11:06 * @description */@Configuration@EnableAspectJAutoProxypublic class BeanConfig {}

UserDaoImpl 增加了 @Repository注解​​​​​​​​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:52 * @description 注解配置改造 */@Repositorypublic class UserDaoImpl {
   public List<User> list(){       return Arrays.asList(new User("",18));   }}

UserService 增加了@Service 注解,并通过@Autowired注入userDao​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 9:52 * @description 注解配置改造 */@Servicepublic class UserService {
    @Autowired    private UserDaoImpl userDao;
    public List<User> list(){        return userDao.list();    }}

LogAspect增加@Component注解​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 10:32 * @description 注解改造 */@Aspect@Componentpublic class LogAspect {
    @Around("execution(* com.bcst.service.*.*.*(..))")    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();        System.out.println("注解方式 execute method:"+method.getName());        return joinPoint.proceed();    }}

测试​​​​​​​

/** * @description  * @author  * @date 2023/8/31 0031 11:10 * @description 注解配置方式改造 */public class UserServiceTest {
    @Test    public void test1(){        ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.bcst");        UserService userService = applicationContext.getBean(UserService.class);        List<User> userList = userService.list();        System.out.println(JSON.toJSON(userList));    }}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值