使用SpringBoot编写RESTful API

什么是RESTful API

简单而言,就是用URL表示资源,HTTP 方法表示动作;

使用JSON进行数据交互

RESTful只是一种风格,不是强制的标准

image-20190313193502120

一般做到Level2就不错了

SpringBoot中使用MockMVC进行测试

首先得保证已经在pom中引入了以下依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
  • 原始测试代码



import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.Assert.*;

/**
 * @Author sherry
 * @Description
 * @Date Create in 2019-03-13
 * @Modified By:
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;
}

至此,就已经构建好了MVC环境,可以模拟Web环境进行测试了

基本Get请求

    @Test
    public void testQuerySuccess() throws Exception {
//        perform:发起请求,然后判断返回结果是否符合我们的期望
        mockMvc.perform(MockMvcRequestBuilders.get("/user/")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
//                要求返回结果状态码为 200
                .andExpect(MockMvcResultMatchers.status().isOk())
//                要求返回结果是一个集合,且长度为3
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }

此时直接执行Test方法,毫无疑问返回状态码为404,因为我还没有编写/user/接口

image-20190313195124316

  • 完善Controller
@RestController
@RequestMapping("/user")
public class UserController {


    @GetMapping
    public ResponseEntity getUserList() {
        List<UserVo> list = Arrays.asList(new UserVo().setName("哇哈哈1")
                , new UserVo().setName("哇哈哈2")
                , new UserVo().setName("哇哈哈3"));
        return ResponseEntity.ok(list);
    }
}

这样测试用例就能通过了

这里的Vo对象,使用lombok进行了便捷操作

@Test
    public void whenGetInfoFail() throws Exception {
        mockMvc.perform(get("/user/a")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().is4xxClientError());
    }

指定返回的是4xx的错误码

添加Param请求参数

    @Test
    public void testQuerySuccess() throws Exception {
//        perform:发起请求,然后判断返回结果是否符合我们的期望
        mockMvc.perform(MockMvcRequestBuilders.get("/user/")
//                添加请求参数
                .param("name", "小混蛋")
				.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
//                要求返回结果状态码为 200
                .andExpect(MockMvcResultMatchers.status().isOk())
//                要求返回结果是一个集合,且长度为3
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }

在Controller中,使用同名基本类型参数,或者带有此名的POJO对象进行接收即可

andExpect中编写期望;一般先判断状态,再判断内容

  • Controller中的代码

public ResponseEntity getUserList(@RequestParam String name)

public ResponseEntity getUserList(UserQueryCondition userQueryCondition)

只要保证查询对象中有name成员变量即可

注意,查询对象前不能添加@RequestParam注解

分页查询

如果使用Spring Data项目作为持久层,那么可以使用Pageable对象来接收分页信息,参数名默认为pagesize

.param("page","3")
.param("size","15")
public ResponseEntity getUserList(UserQueryCondition userQueryCondition
								, Pageable pageable)

使用@PageableDefault指定默认分页信息与排序信息

@PageableDefault(page = 0,size = 15,sort = {"name,desc"}) Pageable pageable

JsonPath表达式

对于返回结果,我们可以使用jsonPath表达式进行灵活的断言,

MockMvcResultMatchers.jsonPath("$.length()").value(3),断言返回结果为长度为3的数组

.andExpect(jsonPath("$.name").value("姓名1")),返回的是一个对象,其中name属性值为姓名1

github

请求JSON对象

如果请求参数不是键值对,而是json对象,那该怎么做呢?

.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(Json字符串)

文件上传

@Test
    public void whenUploadSuccess() throws Exception {
        String result = mockMvc.perform(fileUpload("/file")
                .file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
        System.out.println(result);
    }

  • Controller
    private String folder = "/Users/zhangliuning/Gitee/workspace/imooc-security/imooc-security-demo/target";

    @PostMapping
    public FileInfo upload(MultipartFile file) throws Exception {

        System.out.println(file.getName());
        System.out.println(file.getOriginalFilename());
        System.out.println(file.getSize());

        File localFile = new File(folder, new Date().getTime() + ".txt");

        file.transferTo(localFile);

        return new FileInfo(localFile.getAbsolutePath());
    }

对URL参数使用正则表达式校验

@GetMapping("/{userId:\\d+}")

冒号后面的正则表达式为对userId这个url参数的校验

@ JsonView控制JSON输出内容

  • 返回对象
public class User {

	public interface UserSimpleView {};
	public interface UserDetailView extends UserSimpleView {};

	private String id;
	private String username;
	private String password;
	private Date birthday;

	@JsonView(UserSimpleView.class)
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	@JsonView(UserDetailView.class)
	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@JsonView(UserSimpleView.class)
	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	@JsonView(UserSimpleView.class)
	public Date getBirthday() {
		return birthday;
	}

	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}

}

UserDetailView继承了UserSimpleView,表示UserSimpleView显示的字段UserDetailView也要显示

  • 控制器
@GetMapping
	@JsonView(User.UserSimpleView.class)
	public List<User> query(UserQueryCondition condition,
							@PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) {
......
	}

自定义校验

  • 自定义校验


import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.imooc.security.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;


/**
 * @author zhailiang
 *
 */
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {

    @Autowired
    private HelloService helloService;

    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        System.out.println("my validator init");
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        helloService.greeting("tom");
        System.out.println(value);
        return true;
    }

}

@MyConstraint就可以和标准校验注解一样进行使用了

@MyConstraint(message = "这是一个测试")
private String username;

分组校验

控制器增强处理异常

package com.zhidianfan.ydcommon.frame;

import com.zhidianfan.ydcommon.constant.dto.HttpCommonRes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.support.WebExchangeBindException;

/**
 * 参数校验
 */
@ControllerAdvice
public class ControllerCheckAdvice {


    private Logger log = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(WebExchangeBindException.class)
    public ResponseEntity<HttpCommonRes> checkRequest1(WebExchangeBindException e) {
        return getHttpCommonResResponseEntity(e.getMessage(), e.getBindingResult());
    }


    @ExceptionHandler(BindException.class)
    public ResponseEntity<HttpCommonRes> checkRequest2(BindException e) {
        return getHttpCommonResResponseEntity(e.getMessage(), e.getBindingResult());
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<HttpCommonRes> checkRequest3(RuntimeException e) {
        HttpCommonRes httpCommonRes = new HttpCommonRes(400, "参数校验异常", e.getMessage());
        return ResponseEntity.ok(httpCommonRes);
    }

    private ResponseEntity<HttpCommonRes> getHttpCommonResResponseEntity(String message, BindingResult bindingResult2) {
        log.info(message);

        BindingResult bindingResult = bindingResult2;

        String errorMesssage = "";

        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errorMesssage += fieldError.getField() + ":" + fieldError.getDefaultMessage() + ";";
        }

        HttpCommonRes httpCommonRes = new HttpCommonRes(400, "参数校验异常", errorMesssage.substring(0, errorMesssage.length() - 1));
        return ResponseEntity.badRequest().body(httpCommonRes);
    }

}

SpringBoot的默认异常处理与控制器增强

当服务端产生异常的时候,如404、500等http异常

如果检测到是通过浏览器发起的请求,则返回一个html页面

如果检测到不是通过浏览器发送的请求,返回一个json串,字段为:timestamp、status、error、message、path

默认错误处理在SpringBoot的BasicErrorController

服务拦截

一般用于请求/响应的日志记录、权限控制等

上述两点在微服务中使用网关也能做,但并不是每个Restful服务都是在网关中

所以还是有必要了解学习一下的

对于Spring而言,一般就AOP、拦截器两种方式

如果使用Servlet自己的标准,可以使用Filter过滤器

过滤器:Filter

记录请求/响应的时间

/**
 *
 */
package com.imooc.security.demo.web.filter;

import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * @author zhailiang
 *
 */
@Component
public class TimeFilter implements Filter {

    @Override
    public void destroy() {
        System.out.println("time filter destroy");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("time filter start");
        long start = new Date().getTime();
        chain.doFilter(request, response);
        System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
        System.out.println("time filter finish");
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        System.out.println("time filter init");
    }

}

过滤器在SpringBoot的注册方式有两种

1、注册为Spring Bean,只要能过被@SpringBoot扫描到即可

2、注册到FilterRegistrationBean

第三方过滤器一般用第二种方式进行注册

注意:Filter在MockMVC测试中不会生效

必须启动服务后,通过工具发送请求才行

第二种注册方式如下:

/**
 *
 */
package com.imooc.security.demo.web.config;

import java.util.ArrayList;
import java.util.List;

import com.imooc.security.demo.web.filter.TimeFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;


/**
 * @author zhailiang
 */
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {


    @Bean
    public FilterRegistrationBean timeFilter() {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean();

        TimeFilter timeFilter = new TimeFilter();
        registrationBean.setFilter(timeFilter);

        List<String> urls = new ArrayList<>();
        urls.add("/*");
        registrationBean.setUrlPatterns(urls);

        return registrationBean;

    }

}

过滤器中不需要添加@Component

缺点:不知道该请求是发往哪个控制器的

Spring 拦截器能做到这个需求

拦截器:Interceptor

/**
 *
 */
package com.imooc.web.interceptor;

import java.util.Date;

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

import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;


@Component
public class TimeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("preHandle");

        System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
        System.out.println(((HandlerMethod)handler).getMethod().getName());

        request.setAttribute("startTime", new Date().getTime());
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
        Long start = (Long) request.getAttribute("startTime");
        System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));

    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("afterCompletion");
        Long start = (Long) request.getAttribute("startTime");
        System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
        System.out.println("ex is "+ex);

    }

}

注意:如果在@ControllerAdvice中已经把异常捕获了

那么在拦截器中是收不到异常的,即ex为null

注册拦截器

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private TimeInterceptor timeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timeInterceptor);
    }

}

问题:无法拿到方法的参数值

AOP能做到

切片:AOP

package com.imooc.security.demo.web.aspect;

import java.util.Date;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author zhailiang
 *
 */
@Aspect
@Component
public class TimeAspect {

    @Around("execution(* com.imooc.security.demo.web.controller.UserController.*(..))")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {

        System.out.println("time aspect start");

        Object[] args = pjp.getArgs();
        for (Object arg : args) {
            System.out.println("arg is "+arg);
        }

        long start = new Date().getTime();

        Object object = pjp.proceed();

        System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));

        System.out.println("time aspect end");

        return object;
    }

}

拦截顺序

image-20190313223908483

Filter—>Interceptor—>ControllerAdvice—>Aspect—>Controller

Swagger

  • 依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.8.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.8.0</version>
</dependency>
  • 配置
package org.zln.example.springbootswagger01.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2Configuration {
    @Bean
    public Docket buildDocket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(buildApiInf())
                .select()
                .apis(RequestHandlerSelectors.basePackage("org.zln.example.springbootswagger01.web"))//要扫描的API(Controller)基础包
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo buildApiInf() {
        return new ApiInfoBuilder()
                .title("Spring Boot中使用Swagger2 UI构建API文档")
                .contact("土豆")
                .version("1.0")
                .build();
    }
}

如果是默认配置的话,只要在启动类上添加个@EnableSwagger2即可

在我们的控制器方法、请求参数、返回对象中,使用若干注解进行描述

启动服务后,在 http://ip:port/swagger-ui.html页面中就能看到文档

@Api(value = "计算服务", description = "简单的计算服务,提供加减乘除运算API")

添加在控制器中,描述控制器中的方法类别

@ApiOperation("加法运算")

添加在方法上,描述具体的方法

@ApiParam("被除数")

添加在基本类型参数上

@ApiModel("用户模型")

添加在请求响应对象类上

@ApiModelProperty("ID")

添加在请求响应对象的成员变量上

常用注解

WireMock

WireMock用于伪造服务

从官网下载WireMock服务器,其实就是一个jar包,然后在本地启动

WireMock下载地址
另一个地址

$ java -jar wiremock-standalone-2.14.0.jar

--port: Set the HTTP port number e.g. --port 9999

--https-port: If specified, enables HTTPS on the supplied port.

--bind-address: The IP address the WireMock server should serve from. Binds to all local network adapters if unspecified.

--https-keystore: Path to a keystore file containing an SSL certificate to use with HTTPS. The keystore must have a password of “password”. This option will only work if --https-port is specified. If this option isn’t used WireMock will default to its own self-signed certificate.

--keystore-password: Password to the keystore, if something other than “password”.

--https-truststore: Path to a keystore file containing client certificates. See https and proxy-client-certs for details.

--truststore-password: Optional password to the trust store. Defaults to “password” if not specified.

--https-require-client-cert: Force clients to authenticate with a client certificate. See https for details.

--verbose: Turn on verbose logging to stdout

--root-dir: Sets the root directory, under which mappings and __files reside. This defaults to the current directory.

--record-mappings: Record incoming requests as stub mappings. See record-playback.

--match-headers: When in record mode, capture request headers with the keys specified. See record-playback.

--proxy-all: Proxy all requests through to another base URL e.g. --proxy-all="http://api.someservice.com" Typically used in conjunction with --record-mappings such that a session on another service can be recorded.

--preserve-host-header: When in proxy mode, it passes the Host header as it comes from the client through to the proxied service. When this option is not present, the Host header value is deducted from the proxy URL. This option is only available if the --proxy-all option is specified.

--proxy-via: When proxying requests (either by using –proxy-all or by creating stub mappings that proxy to other hosts), route via another proxy server (useful when inside a corporate network that only permits internet access via an opaque proxy). e.g. --proxy-via webproxy.mycorp.com (defaults to port 80) or --proxy-via webproxy.mycorp.com:8080

--enable-browser-proxying: Run as a browser proxy. See browser-proxying.

--no-request-journal: Disable the request journal, which records incoming requests for later verification. This allows WireMock to be run (and serve stubs) for long periods (without resetting) without exhausting the heap. The --record-mappings option isn’t available if this one is specified.

--container-threads: The number of threads created for incoming requests. Defaults to 10.

--max-request-journal-entries: Set maximum number of entries in request journal (if enabled). When this limit is reached oldest entries will be discarded.

--jetty-acceptor-threads: The number of threads Jetty uses for accepting requests.

--jetty-accept-queue-size: The Jetty queue size for accepted requests.

--jetty-header-buffer-size: The Jetty buffer size for request headers, e.g. --jetty-header-buffer-size 16384, defaults to 8192K.

--async-response-enabled: Enable asynchronous request processing in Jetty. Recommended when using WireMock for performance testing with delays, as it allows much more efficient use of container threads and therefore higher throughput. Defaults to false.

--async-response-threads: Set the number of asynchronous (background) response threads. Effective only with asynchronousResponseEnabled=true. Defaults to 10.

--extensions: Extension class names e.g. com.mycorp.HeaderTransformer,com.mycorp.BodyTransformer. See extending-wiremock.

--print-all-network-traffic: Print all raw incoming and outgoing network traffic to console.

--global-response-templating: Render all response definitions using Handlebars templates.

--local-response-templating: Enable rendering of response definitions using Handlebars templates for specific stub mappings.

--help: Show command line help

设置mapping与文件

/**
 *
 */
package com.imooc.security.demo.web.wiremock;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.removeAllMappings;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;

import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.ClassPathResource;

/**
 * @author zhailiang
 *
 */
public class MockServer {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        configureFor(8062);
        removeAllMappings();

        mock("/order/1", "01");
        mock("/order/2", "02");
    }

    private static void mock(String url, String file) throws IOException {
        ClassPathResource resource = new ClassPathResource("mock/response/" + file + ".txt");
        String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n");
        stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200)));
    }

}

txt文件中其实就是json字符串数据,如

{
    "id":1,
    "type":"C"
}

异步处理

image.png

对于Web容器而言,其能够对外提供的连接数是有限的,
例如:我们的Tomcat设定了1000个线程,在同步处理的模型中,当这1000个线程都在处理外部请求的时候,新的请求就无法被处理了。

为了服务更多的外部请求,我们的主线程收到请求后将请求交给副线程处理,

这个时候主线程能够继续接收外部的请求。

大大提高服务的负载能力

Runnable的方式

import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import java.util.concurrent.Callable;

@RestController
public class AsyncController {

    private Logger logger = LoggerFactory.getLogger(AsyncController.class);

    @RequestMapping("/order")
    public Callable<String> order() throws Exception {
        logger.info("主线程开始");


        Callable<String> result = new Callable<String>() {
            @Override
            public String call() throws Exception {
                logger.info("副线程开始");
                Thread.sleep(1000);//模拟业务逻辑
                logger.info("副线程返回");
                return "success";
            }
        };

        logger.info("主线程返回");
        return result;
    }

}


2018-03-20 22:46:04.846  INFO 8125 --- [nio-8060-exec-8] c.i.s.demo.web.async.AsyncController     : 主线程开始
2018-03-20 22:46:04.846  INFO 8125 --- [nio-8060-exec-8] c.i.s.demo.web.async.AsyncController     : 主线程返回
2018-03-20 22:46:04.846  INFO 8125 --- [      MvcAsync4] c.i.s.demo.web.async.AsyncController     : 副线程开始
2018-03-20 22:46:05.851  INFO 8125 --- [      MvcAsync4] c.i.s.demo.web.async.AsyncController     : 副线程返回

从日志中可以看出,主线程收到请求后立马返回,可以接着处理新的请求

真正的业务处理数放在副线程中的。

而对外部而言,就好像这是一个同步请求的接口一样,完全感知不到任何区别

Runnable方式下的异步处理,副线程是由主线程调起的

在复杂环境下,如下图

image.png

请求和返回在两个线程中,此时Runnable的方式就不合适了

Runnable方式要求副线程是由主线程开启的

使用DeferredResult可以达到上述目标

DeferredResult

@GetMapping("/order3")
public DeferredResult<String> order3() {
    logger.info("主线程开始");

    DeferredResult<String> result = new DeferredResult<>();
    // 异步调用
    LongTimeAsyncCallService longTimeAsyncCallService = new LongTimeAsyncCallService();
    longTimeAsyncCallService.makeRemoteCallAndUnnowWhenFinish(new LongTimeTaskCallback() {
        @Override
        public void callback(Object result1) {
            logger.info("异步调用完成");
            result.setResult(String.valueOf(result1));
        }
    });

    logger.info("主线程返回");
    return result;
}

@GetMapping("/order4")
public DeferredResult<String> order4() {
    logger.info("主线程开始");
    DeferredResult<String> deferredResult = new DeferredResult<>();
    TaskService taskService = new TaskService();
    CompletableFuture.supplyAsync(taskService::execute).whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
    logger.info("主线程结束");
    return deferredResult;
}
interface LongTimeTaskCallback {
    void callback(Object result);
}

class LongTimeAsyncCallService {
    private Logger logger = LoggerFactory.getLogger(LongTimeAsyncCallService.class);
    private final int corePollSize = 4;
    private final int needSeconds = 3;
    private Random random = new Random();
    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(corePollSize);

    public void makeRemoteCallAndUnnowWhenFinish(LongTimeTaskCallback callback) {
        logger.info("完成任务需要:{}秒", needSeconds);
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                callback.callback("长时间异步调用完成");
            }
        }, needSeconds, SECONDS);
    }
}

class TaskService {
    private Logger logger = LoggerFactory.getLogger(TaskService.class);

    public String execute() {
        try {
            logger.info("模拟异步执行的过程");
            Thread.sleep(2000);
            logger.info("异步调用结束");
            return "异步执行的结果";
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

CompletableFuture

CompetableFutureJava的非阻塞异步编程方法

    @GetMapping("/test1")
    public CompletableFuture<String> test1(String id) {
        log.info("开启");
        CompletableFuture<String> r = CompletableFuture.supplyAsync(() -> {

            try {
                log.info("处理中:{}", id);
                TimeUnit.SECONDS.sleep(1);
                return objectMapper.writeValueAsString("哇哈哈");
            } catch (JsonProcessingException|InterruptedException e) {
                e.printStackTrace();
            } 
            return "";
        });
        log.info("结束");
        return r;

    }
2019-01-07 16:26:53.388  INFO 21057 --- [nio-8080-exec-2] ication$$EnhancerBySpringCGLIB$$d35a5647 : 开启
2019-01-07 16:26:53.392  INFO 21057 --- [nio-8080-exec-2] ication$$EnhancerBySpringCGLIB$$d35a5647 : 结束
2019-01-07 16:26:53.392  INFO 21057 --- [onPool-worker-1] ication$$EnhancerBySpringCGLIB$$d35a5647 : 处理中:1

从日志中可以发现,servlet服务器主线程没有被阻塞,这就提高了应用的承载能力

CompletableFuture内部的代码,会开辟一个新的线程进行处理

不过这一切在Spring5中就简单多了,Web-Flux已经内置了对异步方法的处理,不用再手动编写这种代码## 异步拦截器配置
当使用异步的方式进行处理的时候,拦截器的配置是不一样的

覆盖

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
  @Override
  public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    ......
  }
}

此方法

调用registerCallableInterceptorsregisterDeferredResultInterceptors,根据实际情况进行注册

包括异步调用的若干配置,如异步调用的超时实践,也可以通过configurer的相关方法进行配置

另一个就是线程池的配置,使用Runnable方式进行副线程调用的时候,默认每次都是开启新的线程,我们可以使用configurer配置一个线程池

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值