Rest简单导出csv格式数据文件

  • 前言
  • 一、引入HttpServletResponse
  • 依赖注入
  • 工具类
  • 方法参数
  • 二、导出实现
  • 三、基于切面赋能接口
  • 四、效果展示
  • 五、基于HandlerMethod扩展点实现



前言

由于最近开发组件与数据相关,在数据规模越来越大后,传统swagger界面针对大批量的数据查询越来越慢(接口本身性能没问题),为了解决浏览器和swagger界面本身JSON渲染慢的问题,考虑可以通过指定的Rest接口,导出数据文件,方便用于核对数据。

一、引入HttpServletResponse

依赖注入

@Autowired
    private HttpServletRequest request;
    @Autowired
    private HttpServletResponse response;

工具类

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = servletRequestAttributes.getRequest();
    HttpServletResponse response = servletRequestAttributes.getResponse();

方法参数

@GetMapping(value = "")
public String download(HttpServletRequest request,HttpServletResponse response) {
    //...

在方法上加上参数,Springboot就会帮你绑定,你可以直接使用。如果你的方法有其他参数,把这两个参数加到后面即可。

二、导出实现

导出的实现,其实也十分简单,主要就是操作response进行数据写入即可。下面提供最原生的代码示例。

@RequestMapping("/download")
    public void download(@RequestParam("data") String data) {
        response.reset();
        response.addHeader("content-type", "application/octet-stream");
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=" + "output.csv");
        response.setStatus(HttpServletResponse.SC_OK);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 10; i++) {
            String info = "field-" + i;
            try {
                outputStream.write(info.getBytes("UTF-8"));
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (i < 9) {
                try {
                    outputStream.write(",".getBytes("UTF-8"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    outputStream.write("\n".getBytes("UTF-8"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        for (int i = 0; i < 10; i++) {
            String info = "value-" + i;
            try {
                outputStream.write(info.getBytes("UTF-8"));
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (i < 9) {
                try {
                    outputStream.write(",".getBytes("UTF-8"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    outputStream.write("\n".getBytes("UTF-8"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        try {
            if (outputStream != null) {
                outputStream.flush();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

三、基于切面赋能接口

聪明如你,可能已经想到了,每个数据查询接口,都要写一个对应的导出文件接口啊,也太low了吧。确实,很明显可以考虑借助AOP实现批量接口的数据文件导出功能。
增加切面相关的JAR包依赖:

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

定义注解:

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedCsvExport {
}

切面类:

@Aspect
@Component
public class CsvExport {
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private HttpServletResponse response;
    
    @Around("@annotation(NeedCsvExport)))")
    public Object export(ProceedingJoinPoint joinPoint) {
        String header = request.getHeader("csv-export");
        System.out.println(header);
        if (!Boolean.parseBoolean(header)) {
            try {
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
        try {
            ServletOutputStream outputStream = response.getOutputStream();
            response.reset();
            response.addHeader("content-type", "application/octet-stream");
            response.setContentType("application/octet-stream;charset=UTF-8");
            response.addHeader("Content-Disposition", "attachment;filename=" + "out.csv");
            response.setStatus(HttpServletResponse.SC_OK);
            Object proceed = joinPoint.proceed();
            //导入
            StringBuffer head = new StringBuffer();
            Field[] declaredFields = proceed.getClass().getDeclaredFields();
            for (int i = 0; i < declaredFields.length; i++) {
                String name = declaredFields[i].getName();
                if (i < declaredFields.length - 1) {
                    head.append(name).append(",");
                } else {
                    head.append(name);
                }
            }
            writeString(head.toString(), outputStream);
            close(outputStream);
            return null;

        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }

    private void writeString(String content, ServletOutputStream outputStream) throws IOException {
        outputStream.write(content.getBytes(Charset.forName("UTF-8")));
    }

    private void close(ServletOutputStream outputStream) {
        try {
            if (outputStream != null) {
                outputStream.flush();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

从具体的实现中可以看到,如果检测到http请求头中有变化csv-export且为true,则将查询结果导出到csv文件中,并原接口返回null。

使用:

@NeedCsvExport
    @RequestMapping("/push")
    public Result push(@RequestParam("data") String data) {
        String result = loopLongPollingService.push(data);
        return new Result(result);
    }

四、效果展示

第二节实现后,在浏览器输出对应url:http://localhost:18081/loop/download,则直接自动下载csv文件

RestHighLevelClient 导出数据 resform导出数据_spring


内容:

RestHighLevelClient 导出数据 resform导出数据_SpringBoot_02


第三节实现后,请求地址:http://localhost:18081/loop/push?data=1234,不设置head请求头的csv-export,方法正常执行和返回,如图:

RestHighLevelClient 导出数据 resform导出数据_Aop_03


设置csv-export:true后,执行:

RestHighLevelClient 导出数据 resform导出数据_spring_04


点击下载,则会下载结果数据文件。

五、基于HandlerMethod扩展点实现

如下所示:


1. HandlerMethodArgumentResolver
2. HandlerMethodArgumentResolverComposite
3. HandlerMethodReturnValueHandler
4. HandlerMethodReturnValueHandlerComposite

在接口org.springframework.web.servlet.config.annotation.WebMvcConfigurer的方法addReturnValueHandlers和addArgumentResolvers可以加入上面具体Resolver的实现,通过该处扩展点则可以叠加对于方法执行的控制。当然WebMvcConfigurer还有很多扩展点可以使用,如:

public interface WebMvcConfigurer {
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }
    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }
    default void addFormatters(FormatterRegistry registry) {
    }
    default void addInterceptors(InterceptorRegistry registry) {
    }
    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }
    default void addCorsMappings(CorsRegistry registry) {
    }
    default void addViewControllers(ViewControllerRegistry registry) {
    }
    default void configureViewResolvers(ViewResolverRegistry registry) {
    }
    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }
    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }
    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }
    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }
    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }
    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }
    @Nullable
    default Validator getValidator() {
        return null;
    }
    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

代码示例:

import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.example.demo.control.NeedCsvExport;

public class NeedCsvReturnValueHandler implements HandlerMethodReturnValueHandler {
    @Override
    public boolean supportsReturnType(MethodParameter methodParameter) {
        return methodParameter.getMethodAnnotation(NeedCsvExport.class) != null;
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest) throws Exception {
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        NeedCsvExport responseExcel = methodParameter.getMethodAnnotation(NeedCsvExport.class);
        Assert.state(responseExcel != null, "No @NeedCsvExport");
        modelAndViewContainer.setRequestHandled(true);
        response.reset();
        response.addHeader("content-type", "application/octet-stream");
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=" + "out.csv");
        response.setStatus(HttpServletResponse.SC_OK);
        //导入
        StringBuffer head = new StringBuffer();
        Field[] declaredFields = returnValue.getClass().getDeclaredFields();
        for (int i = 0; i < declaredFields.length; i++) {
            String name = declaredFields[i].getName();
            if (i < declaredFields.length - 1) {
                head.append(name).append(",");
            } else {
                head.append(name);
            }
        }
        ServletOutputStream outputStream = response.getOutputStream();
        writeString(head.toString(), outputStream);
        close(outputStream);
    }

    private void writeString(String content, ServletOutputStream outputStream) throws IOException {
        outputStream.write(content.getBytes(Charset.forName("UTF-8")));
    }

    private void close(ServletOutputStream outputStream) {
        try {
            if (outputStream != null) {
                outputStream.flush();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

通过配置注入上述Handler

import java.util.List;

import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class NeedCsvWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        handlers.add(new NeedCsvReturnValueHandler());
    }
}
@Configuration
public class ThreadPoolConfig {
    @Bean
    public WebMvcConfigurer getWebMvcConfigurer() {
        NeedCsvWebMvcConfigurer configurer = new NeedCsvWebMvcConfigurer();
        return configurer;
    }
}

通过直接定义成Component注入,只需要在NeedCsvWebMvcConfigurer增加Component注解即可。

@Component
public class NeedCsvWebMvcConfigurer implements WebMvcConfigurer {

而CSV格式或xls格式文件的导出,可以借助开源项目做到更优雅导入导出或直接实现自定义的starter

ps.需要注意的是HandlerMethodReturnValueHandler处理器,针对@RestController注解类中方法是无效的