在Spring异步调用中传递上下文的方法

什么是异步调用?

异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行。异步调用指,在程序在执行时,无需等待执行的返回值即可继续执行后面的代码。在我们的应用服务中,有很多业务逻辑的执行操作不需要同步返回(如发送邮件、冗余数据表等),只需要异步执行即可。

本文将介绍 Spring 应用中,如何实现异步调用。在异步调用的过程中,会出现线程上下文信息的丢失,我们该如何解决线程上下文信息的传递。

Spring 应用中实现异步

Spring 为任务调度与异步方法执行提供了注解支持。通过在方法或类上设置 @Async 注解,可使得方法被异步调用。调用者会在调用时立即返回,而被调用方法的实际执行是交给 Spring 的 TaskExecutor 来完成的。所以被注解的方法被调用的时候,会在新的线程中执行,而调用它的方法会在原线程中执行,这样可以避免阻塞,以及保证任务的实时性。

引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入 Spring 相关的依赖即可。

入口类

@SpringBootApplication
@EnableAsync
public class AsyncApplication {
  public static void main(String[] args) {
    SpringApplication.run(AsyncApplication.class, args);
  }

```
入口类增加了 `@EnableAsync` 注解,主要是为了扫描范围包下的所有 `@Async` 注解。
#### 对外的接口
这里写了一个简单的接口:

```java
@RestController
@Slf4j
public class TaskController {

  @Autowired
  private TaskService taskService;

  @GetMapping("/task")
  public String taskExecute() {
    try {
      taskService.doTaskOne();
      taskService.doTaskTwo();
      taskService.doTaskThree();
    } catch (Exception e) {
      log.error("error executing task for {}",e.getMessage());
    }
    return "ok";
  }
}

调用 TaskService 执行三个异步方法。

Service 方法

@Component
@Slf4j
//@Async
public class TaskService {

  @Async
  public void doTaskOne() throws Exception {
    log.info("开始做任务一");
    long start = System.currentTimeMillis();
    Thread.sleep(1000);
    long end = System.currentTimeMillis();
    log.info("完成任务一,耗时:" + (end - start) + "毫秒");
  }

  @Async
  public void doTaskTwo() throws Exception {
    log.info("开始做任务二");
    long start = System.currentTimeMillis();
    Thread.sleep(1000);
    long end = System.currentTimeMillis();
    log.info("完成任务二,耗时:" + (end - start) + "毫秒");
  }

  @Async
  public void doTaskThree() throws Exception {
    log.info("开始做任务三");
    long start = System.currentTimeMillis();
    Thread.sleep(1000);
    long end = System.currentTimeMillis();
    log.info("完成任务三,耗时:" + (end - start) + "毫秒");
  }
}

@Async 可以用于类上,标识该类的所有方法都是异步方法,也可以单独用于某些方法。每个方法都会 sleep 1000 ms。

结果展示

运行结果如下:

可以看到 TaskService 中的三个方法是异步执行的,接口的结果快速返回,日志信息异步输出。异步调用,通过开启新的线程调用的方法,不影响主线程。异步方法实际的执行交给了 Spring 的 TaskExecutor 来完成。

Future:获取异步执行的结果

在上面的测试中我们也可以发现主调用方法并没有等到调用方法执行完就结束了当前的任务。如果想要知道调用的三个方法全部执行完该怎么办呢,下面就可以用到异步回调。

异步回调就是让每个被调用的方法返回一个 Future 类型的值,Spring 中提供了一个 Future 接口的子类:AsyncResult,所以我们可以返回 AsyncResult 类型的值。

public class AsyncResult<V> implements ListenableFuture<V> {

 private final V value;

 private final ExecutionException executionException;
 //...
}

AsyncResult 实现了 ListenableFuture 接口,该对象内部有两个属性:返回值和异常信息。

public interface ListenableFuture<T> extends Future<T> {
  void addCallback(ListenableFutureCallback<? super T> var1);

  void addCallback(SuccessCallback<? super T> var1, FailureCallback var2);
}

ListenableFuture 接口继承自 Future,在此基础上增加了回调方法的定义。Future 接口定义如下:

public interface Future<V> {
 // 是否可以打断当前正在执行的任务
  boolean cancel(boolean mayInterruptIfRunning);

  // 任务取消的结果
  boolean isCancelled();

 // 异步方法中最后返回的那个对象中的值
 V get() throws InterruptedException, ExecutionException;
 // 用来判断该异步任务是否执行完成,如果执行完成,则返回 true,如果未执行完成,则返回false
  boolean isDone();
 // 与 get() 一样,只不过这里参数中设置了超时时间
  V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;
}

#get() 方法,在执行的时候是需要等待回调结果的,阻塞等待。如果不设置超时时间,它就阻塞在那里直到有了任务执行完成。我们设置超时时间,就可以在当前任务执行太久的情况下中断当前任务,释放线程,这样就不会导致一直占用资源。

#cancel(boolean) 方法,参数是一个 boolean 类型的值,用来传入是否可以打断当前正在执行的任务。如果参数是 true 且当前任务没有执行完成 ,说明可以打断当前任务,那么就会返回 true;如果当前任务还没有执行,那么不管参数是 true 还是 false,返回值都是 true;如果当前任务已经完成,那么不管参数是 true 还是 false,那么返回值都是 false;如果当前任务没有完成且参数是 false,那么返回值也是 false。即:

  1. 如果任务还没执行,那么如果想取消任务,就一定返回 true,与参数无关。
  2. 如果任务已经执行完成,那么任务一定是不能取消的,所以此时返回值都是false,与参数无关。
  3. 如果任务正在执行中,那么此时是否取消任务就看参数是否允许打断(true/false)。

获取异步方法返回值的实现

public Future<String> doTaskOne() throws Exception {
  log.info("开始做任务一");
  long start = System.currentTimeMillis();
  Thread.sleep(1000);
  long end = System.currentTimeMillis();
  log.info("完成任务一,耗时:" + (end - start) + "毫秒");
  return new AsyncResult<>("任务一完成,耗时" + (end - start) + "毫秒");
}
//...其他两个方法类似,省略

我们将 task 方法的返回值改为 Future<String>,将执行的时间拼接为字符串返回。

@GetMapping("/task")
public String taskExecute() {
  try {
    Future<String> r1 = taskService.doTaskOne();
    Future<String> r2 = taskService.doTaskTwo();
    Future<String> r3 = taskService.doTaskThree();
    while (true) {
      if (r1.isDone() && r2.isDone() && r3.isDone()) {
        log.info("execute all tasks");
        break;
      }
      Thread.sleep(200);
    }
    log.info("\n" + r1.get() + "\n" + r2.get() + "\n" + r3.get());
  } catch (Exception e) {
    log.error("error executing task for {}",e.getMessage());
  }

  return "ok";
}

在调用异步方法之后,可以通过循环判断异步方法是否执行完成。结果正如我们所预期,future 所 get 到的是 AsyncResult 返回的字符串。

配置线程池

前面是最简单的使用方法,使用默认的 TaskExecutor。如果想使用自定义的 Executor,可以结合 @Configuration 注解的配置方式。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class TaskPoolConfig {

  @Bean("taskExecutor") // bean 的名称,默认为首字母小写的方法名
  public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10); // 核心线程数(默认线程数)
    executor.setMaxPoolSize(20); // 最大线程数
    executor.setQueueCapacity(200); // 缓冲队列数
    executor.setKeepAliveSeconds(60); // 允许线程空闲时间(单位:默认为秒)
    executor.setThreadNamePrefix("taskExecutor-"); // 线程池名前缀
    // 线程池对拒绝任务的处理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
  }
}

线程池的配置很灵活,对核心线程数、最大线程数等属性进行配置。其中,rejection-policy,当线程池已经达到最大线程数的时候,如何处理新任务。可选策略有 CallerBlocksPolicy、CallerRunsPolicy 等。CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行。我们验证下,线程池的设置是否生效,在 TaskService 中,打印当前的线程名称:

public Future<String> doTaskOne() throws Exception {
  log.info("开始做任务一");
  long start = System.currentTimeMillis();
  Thread.sleep(1000);
  long end = System.currentTimeMillis();
  log.info("完成任务一,耗时:" + (end - start) + "毫秒");
  log.info("当前线程为 {}", Thread.currentThread().getName());
  return new AsyncResult<>("任务一完成,耗时" + (end - start) + "毫秒");
}

通过结果可以看到,线程池配置的线程名前缀已经生效。在 Spring @Async 异步线程使用过程中,需要注意的是以下的用法会使 @Async 失效:

  1. 异步方法使用 static 修饰;
  2. 异步类没有使用 @Component 注解(或其他注解)导致 Spring 无法扫描到异步类;
  3. 异步方法不能与被调用的异步方法在同一个类中;
  4. 类中需要使用 @Autowired 或 @Resource 等注解自动注入,不能手动 new 对象;
  5. 如果使用 Spring Boot 框架必须在启动类中增加 @EnableAsync 注解。

线程上下文信息传递

很多时候,在微服务架构中的一次请求会涉及多个微服务。或者一个服务中会有多个处理方法,这些方法有可能是异步方法。有些线程上下文信息,如请求的路径,用户唯一的 userId,这些信息会一直在请求中传递。如果不做任何处理,我们看下是否能够正常获取这些信息。

@GetMapping("/task")
  public String taskExecute() {
    try {
      Future<String> r1 = taskService.doTaskOne();
      Future<String> r2 = taskService.doTaskTwo();
      Future<String> r3 = taskService.doTaskThree();

      ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      HttpServletRequest request = requestAttributes.getRequest();
      log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
      while (true) {
        if (r1.isDone() && r2.isDone() && r3.isDone()) {
          log.info("execute all tasks");
          break;
        }
        Thread.sleep(200);
      }
      log.info("\n" + r1.get() + "\n" + r2.get() + "\n" + r3.get());
    } catch (Exception e) {
      log.error("error executing task for {}", e.getMessage());
    }

    return "ok";
  }

在 Spring Boot Web 中我们可以通过 RequestContextHolder 很方便的获取 request。在接口方法中,输出请求的方法和请求的路径。

public Future<String> doTaskOne() throws Exception {
  log.info("开始做任务一");
  long start = System.currentTimeMillis();
  Thread.sleep(1000);
  long end = System.currentTimeMillis();
  log.info("完成任务一,耗时:" + (end - start) + "毫秒");
  ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  HttpServletRequest request = requestAttributes.getRequest();
  log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
  return new AsyncResult<>("任务一完成,耗时" + (end - start) + "毫秒");
}

同时在 TaskService 中,验证是不是也能输出请求的信息。运行程序,结果如下:

在 TaskService 中,每个异步线程的方法获取 RequestContextHolder 中的请求信息时,报了空指针异常。这说明了请求的上下文信息未传递到异步方法的线程中。RequestContextHolder 的实现,里面有两个 ThreadLocal 保存当前线程下的 request。

//得到存储进去的request
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
    new NamedThreadLocal<RequestAttributes>("Request attributes");
//可被子线程继承的request
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
    new NamedInheritableThreadLocal<RequestAttributes>("Request context");

再看 #getRequestAttributes() 方法,相当于直接获取 ThreadLocal 里面的值,这样就使得每一次获取到的 Request 是该请求的 request。如何将上下文信息传递到异步线程呢?Spring 中的 ThreadPoolTaskExecutor 有一个配置属性 TaskDecorator,TaskDecorator 是一个回调接口,采用装饰器模式。装饰模式是动态的给一个对象添加一些额外的功能,就增加功能来说,装饰模式比生成子类更为灵活。因此 TaskDecorator 主要用于任务的调用时设置一些执行上下文,或者为任务执行提供一些监视/统计。

public interface TaskDecorator {

 Runnable decorate(Runnable runnable);
}

#decorate 方法,装饰给定的 Runnable,返回包装的 Runnable 以供实际执行。

下面我们定义一个线程上下文拷贝的 TaskDecorator。

import org.springframework.core.task.TaskDecorator;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

public class ContextDecorator implements TaskDecorator {
  @Override
  public Runnable decorate(Runnable runnable) {
    RequestAttributes context = RequestContextHolder.currentRequestAttributes();
    return () -> {
      try {
        RequestContextHolder.setRequestAttributes(context);
        runnable.run();
      } finally {
        RequestContextHolder.resetRequestAttributes();
      }
    };
  }
}

实现较为简单,将当前线程的 context 装饰到指定的 Runnable,最后重置当前线程上下文。

在线程池的配置中,增加回调的 TaskDecorator 属性的配置:

@Bean("taskExecutor")
public Executor taskExecutor() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(10);
  executor.setMaxPoolSize(20);
  executor.setQueueCapacity(200);
  executor.setKeepAliveSeconds(60);
  executor.setThreadNamePrefix("taskExecutor-");
  executor.setWaitForTasksToCompleteOnShutdown(true);
  executor.setAwaitTerminationSeconds(60);
  // 增加 TaskDecorator 属性的配置
  executor.setTaskDecorator(new ContextDecorator());
  executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  executor.initialize();
  return executor;
}

经过如上配置,我们再次运行服务,并访问接口,控制台日志信息如下:

由结果可知,线程的上下文信息传递成功。

小结

本文结合示例讲解了 Spring 中实现异步方法,获取异步方法的返回值。并介绍了配置 Spring 线程池的方式。最后介绍如何在异步多线程中传递线程上下文信息。线程上下文传递在分布式环境中会经常用到,比如分布式链路追踪中需要一次请求涉及到的 TraceId、SpanId。简单来说,需要传递的信息能够在不同线程中。异步方法是我们在日常开发中用来多线程处理业务逻辑,这些业务逻辑不需要严格的执行顺序。用好异步解决问题的同时,更要用对异步多线程的方式。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • springboot 使用上下文获取bean

    问题 在使用springboot开发项目过程中,有些时候可能出现说会有在spring容器加载前就需要注入bean的类,这个时候如果直接使用@Autowire注解,则会出现控制针异常! 解决办法 如下: 创建一个springContextUtil类 package cn.eangaie.appcloud.util; import org.springframework.context.ApplicationContext; public class SpringContextUtil { priv

  • springboot无法从静态上下文中引用非静态变量的解决方法

    静态方法可以不用创建对象就调用,非静态方法必须有了对象的实例才能调用. 因此想在静态方法中直接引用非静态方法是不可能的,因为不知道调用哪个对象的非静态方法,编译器不可能给出答案,因为没有对象. java就怕找不到对象. 解决办法: spring的set注入方法,通过非静态的setter方法注入静态变量,样例如下 @PropertySource(value = {"classpath:config/application.yml"},ignoreResourceNotFound = tr

  • spring boot中使用@Async实现异步调用任务

    什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行:异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序.  同步调用 下面通过一个简单示例来直观的理解什么是同步调用: 定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内) package com.kfit.task; import java.uti

  • Spring Boot利用@Async异步调用:ThreadPoolTaskScheduler线程池的优雅关闭详解

    前言 之前分享了一篇关于Spring Boot中使用@Async来实现异步任务和线程池控制的文章:<Spring Boot使用@Async实现异步调用:自定义线程池>.由于最近身边也发现了不少异步任务没有正确处理而导致的不少问题,所以在本文就接前面内容,继续说说线程池的优雅关闭,主要针对ThreadPoolTaskScheduler线程池. 问题现象 在上篇文章的例子Chapter4-1-3中,我们定义了一个线程池,然后利用@Async注解写了3个任务,并指定了这些任务执行使用的线程池.在上文

  • spring boot 使用@Async实现异步调用方法

    使用@Async实现异步调用 什么是"异步调用"与"同步调用" "同步调用"就是程序按照一定的顺序依次执行,,每一行程序代码必须等上一行代码执行完毕才能执行:"异步调用"则是只要上一行代码执行,无需等待结果的返回就开始执行本身任务. 通常情况下,"同步调用"执行程序所花费的时间比较多,执行效率比较差.所以,在代码本身不存在依赖关系的话,我们可以考虑通过"异步调用"的方式来并发执行. &q

  • 详解在SpringBoot应用中获取应用上下文方法

    1.定义上下文工具类: package com.alimama.config; import org.springframework.context.ApplicationContext; /** * 上下文获取工具类 * @author mengfeiyang * */ public class SpringContextUtil { private static ApplicationContext applicationContext; public static void setAppl

  • 详解SpringBoot中异步请求和异步调用(看完这一篇就够了)

    一.SpringBoot中异步请求的使用 1.异步请求与同步请求 特点: 可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应.一句话:增加了服务器对客户端请求的吞吐量(实际生产上我们用的比较少,如果并发请求量很大的情况下,我们会通过nginx把请求负载到集群服务的各个节点上来分摊请求压力,当然还可以通过消息队列来做请求的缓冲). 2.异步请求的实现 方式一:Servlet方式实现异步请求

  • 深入理解spring boot异步调用方式@Async

    本文主要给大家介绍了关于spring boot异步调用方式@Async的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 1.使用背景 在日常开发的项目中,当访问其他人的接口较慢或者做耗时任务时,不想程序一直卡在耗时任务上,想程序能够并行执行,我们可以使用多线程来并行的处理任务,也可以使用spring提供的异步处理方式@Async. 2.异步处理方式 调用之后,不返回任何数据. 调用之后,返回数据,通过Future来获取返回数据 3.@Async不返回数据 使用@EnableAsyn

  • 在Spring异步调用中传递上下文的方法

    什么是异步调用? 异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行.异步调用指,在程序在执行时,无需等待执行的返回值即可继续执行后面的代码.在我们的应用服务中,有很多业务逻辑的执行操作不需要同步返回(如发送邮件.冗余数据表等),只需要异步执行即可. 本文将介绍 Spring 应用中,如何实现异步调用.在异步调用的过程中,会出现线程上下文信息的丢失,我们该如何解决线程上下文信息的传递. Sprin

  • SpringBoot 异步线程间传递上下文方式

    目录 异步线程间传递上下文 需求 实现 启用异步功能 配置异步 配置任务装饰器 启用多线程安全上下文无法在线程间共享问题 问题 解决方案 原理 结果 异步线程间传递上下文 需求 SpringBoot项目中,经常使用@Async来开启一个子线程来完成异步操作.主线程中的用户信息需要传递给子线程 实现 启用异步功能 在启动类里加上@EnableAsync注解 @EnableAsync @SpringBootApplication public class Application {} 配置异步 新建

  • java异步调用的4种实现方法

    目录 一.利用多线程 直接new线程 使用线程池 二.采用Spring的异步方法去执行(无返回值) @Async注解可以用在方法上,也可以用在类上,用在类上,对类里面所有方法起作用 三.采用Spring的异步方法+Future接收返回值 如果调用之后接收返回值,不对返回值进行操作则为异步操作,进行操作则转为同步操作,等待对返回值操作完之后,才会继续执行主进程下面的流程 四.原生Future方法 参考 一.利用多线程 直接new线程 Thread t = new Thread(){ @Overri

  • ajax调用中ie缓存问题解决方法

    本文实例分析了ajax调用中ie缓存问题解决方法.分享给大家供大家参考,具体如下: ajax请求调用的过程中发现的问题:后台请求是一个简单的.aspx文件,而这个页面又没有考虑过缓存的影响,使用ajax调试的时候发现有时候根本不走后台代码直接返回结果了,所以估计是受到浏览器缓存的影响.网上搜了一下,果然是缓存的问题:"IE中如果XMLHttpRequest提交的URL与历史一样则使用缓存,根本不向服务器端提交.因此无法取到刚提交的数据或新的数据". 解决方法大致有下面几种: 1.只改进

  • Ajax客户端异步调用服务端的实现方法(js调用cs文件)

    ajax的使用方法,在js中调用cs文件中的一直方式,使用步骤如下 (1)下载ajax.dll,并添加项目的引用. (2)在项目的webconfig的<httpHandlers>节点中,添加<add verb="POST,GET" path="ajax/*.ashx" type="Ajax.PageHandlerFactory, Ajax"/>节点 (3)在aspx页面的pageload方法中添加Ajax.Utility.

  • AngularJS入门教程二:在路由中传递参数的方法分析

    本文实例讲述了AngularJS在路由中传递参数的方法.分享给大家供大家参考,具体如下: 我们不仅可以在控制器中直接定义属性的值,比如: app.controller('listController',function($scope){ $scope.name="ROSE"; }); AngularJS还提供了传递参数的功能,目前我接触到的一种方式是从视图中传参: <!--首页html--> <li><a href="#/user/18"

  • spring异步service中处理线程数限制详解

    情况简介 spring项目,controller异步调用service的方法,产生大量并发. 具体业务: 前台同时传入大量待翻译的单词,后台业务接收单词,并调用百度翻译接口翻译接收单词并将翻译结果保存到数据库,前台不需要实时返回翻译结果. 处理方式: controller接收文本调用service中的异步方法,将单词先保存到队列中,再启动2个新线程,从缓存队列中取单词,并调用百度翻译接口获取翻译结果并将翻译结果保存到数据库. 本文主要知识点: 多线程同时(异步)调用方法后,开启新线程,并限制线程

  • Spring在代码中获取bean的方法小结

    一.通过Spring提供的ContextLoader WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext(); wac.getBean(beanID); 这种方式不依赖于servlet,不需要注入的方式.但是需要注意一点,在服务器启动时,Spring容器初始化时,不能通过这种方法获取Spring容器 二.实现接口ApplicationContextAware 定义工具类 public class Sp

  • js中传递特殊字符(+,&)的方法

    背景: 今天在做一个任务时,用Jquery的Ajax传递一长串字符时,在后台的验证一直不成功,纠结时我了(那个字符串是随机生成的,特长).查了一上午,原来是我生成的字符串中有+号,而在js传递的时候,会理解为是连接字符用的,到了后台就将+号自动变为空格了,所以后台的字符串和前台生成的已经不一样了. 原因: js后自动解析特殊字符,如+号为连接符,解析为空格,&为变量连接符,服务器端接受数据时&以后的数据不显示等等. 解决办法: 1.将字符放到form中,然后用js提交form表单到服务器.

  • spring boot项目中MongoDB的使用方法

    前言 大家都知道MySQL数据库很好用,但数据量到了千万以上了,想增加字段是非常痛苦的,这个在MongoDB里就不存在,字段想怎么加就怎么加,所以也就有了想在spring-boot里用MongoDB的想法了,Github上spring-projects里有关于使用MongoDB的demo,后面会给出链接 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring

随机推荐