【解耦Excel导出服务】开发日志

一、开发分析

1、导出excel设计分析

  • 瓶颈:大量数据导出时
    • mysql查询连接占用时间过长
    • 组装对象生成excel时,容易导致CPU占用过高、JVM临时内存占用过大(可能导致oom,可能导致GC)
  • 设计:
    • 解耦Excel导出模块:通过加密api,暴露导出服务至内网
    • 限制每次导出数量:通过查询条件、阈值配置(数量阈值-待测)

2、设计实现(springBoot+mybatis+mysql)

【关键】

  • Excel导出方法
    • 采用org.apache.poi辅助jar包(3.14),组装生成excel流,直接附加到response字节流outputStream中返回
    • 为减小服务器压力,服务端不保存文件,直接返回流
  • 签名校验机制(弃用dubbo交互,采用http-api交互:因为dubbo传输需要将数据先序列化,导出数据较大,序列化传输性能不好;api则可直接传输流)
    • 客户端签名生成:
      • 先将约定密码进行md5加密后去后16位做key
      • 用key将当前时间戳timestamp,进行AES加密,得到32位签名sign(如7abf20cfb8c365c77c01e7904998444e)
      • 将生成签名附在header(key=”sign”)中
    • 服务端签名校验:(1、校验签名是否合法2、校验签名中时间戳是否过期)
      • 在拦截器Interceptor中进行校验
      • 获取Header中的签名sign,用约定密码解密,获得timestamp
      • 校验可否成功解密sign(保证签名合法)
      • 校验签名时间timestamp是否在限定时间内(保证一个签名过期作废,别人拿到也没用)
  • 多数据源(支持并行访问各db)
    • 每个数据源单独配置1套DataSource、SqlSessionFactory、DataSourceTransactionManager
    • 每个数据源单独搭配1套Mapper.java、Mapper.xml、MVC三层

【其他】

  • 调用端
    • 采用CloseableHttpClient实例,进行api调用
  • 参数沟通机制:
    • 附属在header中传递,设定key=”param”(相对于问号传值/路径传值,更简洁)
    • 通过Map格式传递(为防止中文乱码,在client端通过URLEncoder.encode做UTF-8编码,在sever端拦截器中通过URLDecoder.decode做UTF-8解码)
  • 浏览器端
    • 点击按钮,在新标签页打开下载窗口(临时生成target=_blank的a标签,然后点击,比window.open兼容性好)
    • 参数最后附加随机数(防止短间隔多次点击不进cotroller,无法响应)
    • 中文下载文件名兼容chrome、IE、firefox(在client端对header内中文文件名,当FF浏览器访问时做iso-8859-1编码,其他做UTF-8编码)
  • 可识别异常枚举:
    • statusCodeMap.put(“102”, “该api正在处理中”);
      statusCodeMap.put(“400”, “签名生成失败”);
      statusCodeMap.put(“403”, “查询条件不符合规定”);
      statusCodeMap.put(“404”, “找不到该api”);
      statusCodeMap.put(“406”, “签名校验失败”);
      statusCodeMap.put(“405”, “超过最大导出条数限制”);
      statusCodeMap.put(“408”, “请求中的签名超时”);
      statusCodeMap.put(“500”, “下载服务内部异常”);
      statusCodeMap.put(“503”, “下载服务api无法访问”);

3、小优化

a.返回处理状态
b.同个api并发访问/重复提交的处理(采用新开窗口)
c.文件名及下载方式兼容:chrome、IE、firefox,导出Excel后缀采用*.xls,以兼容win10、mac
d.监听下载进度(后续实现)
设计方案-1.对每个下载做唯一标识,开一个Map集合,记录每个下载进度;
2.在组装Excel循环中,定期更新Map集合中的进度,100%后remove该标识
3.单开异步请求(该处暂无方案),间歇访问进度集合,在进度条中显示

e.暂支持最大导出条数阈值(1W),支持不停服调整阈值
性能测试:本地导出1W条-5次:
cpu占用不超过50%
内存占用不超过300M


4、经验教训

  • InputStream转byte[]
public  static  final  byte [ ] input2byte ( InputStream inStream )  throws  IOException  {
ByteArrayOutputStream swapStream  =  new  ByteArrayOutputStream ( ) ;
byte [ ] buff  =  new  byte [ 100 ] ;
int rc  =  0 ;
while  ( (rc  = inStream. read (buff,  0100 ) )  >  0 )  {
swapStream. write (buff,  0, rc ) ;
}
byte [ ] in2b  = swapStream. toByteArray ( ) ;
return in2b ;
}
  • 读取字节输入流InputStream,填充字节输出流OutputStream
// 定义参数
OutputStream out  =  null ;
try  {
// 转移填充文件流
InputStream inputStream  = getResponse. getEntity ( ). getContent ( ) ;
out  = response. getOutputStream ( ) ; // 拿到HttpServletResponse输出流索引
out. write (IOUtil. input2byte (inputStream ) ) ; // 将查到的流,转写入外层响应流(此辅助方法引用上一点方法)
}  catch  ( Exception e )  {
log. error ( "##导出Excel-请求失败"  + e. getMessage ( ), e ) ;
}  finally  {
try  {
if  (out  !=  null )  {
out. flush ( ) ;
out. close ( ) ;
}
}  catch  ( IOException e )  {
log. warn ( "OutputStream关闭失败(一般为中途点击了取消)" ) ;
}
}
原因分析:多数据源报错异常
解决方案:在dataSource1方法上 添加@Primary注解,指定默认数据源,spring便不再报错
@Bean(name = "dataSource1", autowire = Autowire.BY_NAME)
@Primary
public DruidDataSource dataSource1() {
DruidDataSource ds = new DruidDataSource();
。。。
}
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table ‘tool-db.xxx’ doesn’t exist
    ### The error may exist in file [D:\dev\workspace-mars\springBoot-mybatis\target\classes\com\demo\dao\mapper\xxx.xml]
    ### The error may involve defaultParameterMap
    ### The error occurred while setting parameters

原因分析:第二个数据源未能成功引用,导致SqlSessionFactory实例化时,dataSource2尚未扫入容器,导致找不找到数据库
解决方案:在方法sqlSessionFactoryBean2中,注入dataSource实例时,添加@Qualifier(ConfigParam.dataSource2) 注解,显示指定数据源实例

@Bean(name = ConfigParam.sqlSessionFactory2)
public SqlSessionFactory sqlSessionFactoryBean2(@Qualifier(ConfigParam.dataSource2) DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
  • Cannot resolve reference to bean ‘sqlSessionFactory’ while setting bean property ‘sqlSessionFactory’;
    nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException:
    Error creating bean with name ‘sqlSessionFactory’:
    Requested bean is currently in creation: Is there an unresolvable circular reference? 
原因分析:java config配置引起的bean相互引用日志报警告问题
@ConditionalOnClass({SqlSessionFactory.class,SqlSessionFactoryBean.class})
在使用单个mapper时没有报这个警告错误,但是当把mapper增加为2个以上时,就会报该异常信息,不知具体的原因。
跟踪代码,看到会优先执行MyBatisMapperScannerConfig扫描,虽然设置了AutoConfigureAfter,MyBatisMapperScannerConfig修改definition.setBeanClass(MapperFactoryBean.class)后,spring在做dataSourceInitializerPostProcessor处理时,会抛出该异常,不知道哪里出了问题。而且会重复出现2,3边同样的WARN,百分百重现。
解决方案:这个bug是mybatis-spring的,换到最新的1.2.4版本及以上就没问题了。
>
>org.mybatis >
>mybatis-spring >
>1.2.5 >
>

详细内容请直接到git讨论区:mybatis/spring#58

  • (ActionInterceptor.java:136) ERROR – java.lang.IllegalStateException: STREAM
    java.lang.IllegalStateException: STREAM
    at org.eclipse.jetty.server.Response.getWriter(Response.java:717)
    at org.apache.struts2.views.freemarker.FreemarkerResult.getWriter(FreemarkerResult.java:263)

原因排查:
这是web容器生成的servlet代码中有out.write(””),这个和JSP中调用的response.getOutputStream()产生冲突.
Servlet规范说明,不能既调用 response.getOutputStream(),又调用response.getWriter(),无论先调用哪一个,在调用第二个时候应会抛出 IllegalStateException,
因为在jsp中,out变量是通过response.getWriter得到的,在程序中既用了response.getOutputStream,又用了out变量,故出现以上错误。
解决方案:当处理成功是,return null;

  • Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property ‘urlPrefix’ of bean class [com.qz.tools.utils.exportclient.ClientProperties]: Bean property ‘urlPrefix’ is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
    at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:1044)
    at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:904)

原因排查:在spring配置文件中,给对象静态变量注入值失败,低版本spring 不允许/不支持把值注入到静态变量中
解决方案:在不改变spring版本的情况下,可以利用非静态setter方法注入静态变量,即public void setUrlPrefix…….
or 升级spring版本

  • org.springframework.context.ApplicationContextException: Unable to start embedded container; 
    nested exception is org.springframework.context.ApplicationContextException: 
    Unable to start EmbeddedWebApplicationContext due to missing EmbeddedServletContainerFactory bean.
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.onRefresh(EmbeddedWebApplicationContext.java:133) ~

原因排查:springBoot引用内嵌tomcat时,配置了provided,去掉该scope即可

org.springframework.boot
spring-boot-starter-tomcat

  • org.springframework.beans.factory.BeanCreationException:
    Error creating bean with name ‘entityManagerFactory’ defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]:
    Invocation of init method failed; nested exception is javax.persistence.PersistenceException:
    Unable to resolve persistence unit root URL
    Caused by: javax.persistence.PersistenceException: Unable to resolve persistence unit root URL
    Caused by: java.io.FileNotFoundException: class path resource [] cannot be resolved to URL because it does not exist

原因排查:未配置jpa,却在pom中引用了springboot-jpa的jar包
解决方案:去掉springboot-jpa包引用即可

  • JavaScript 获取当前时间戳

var timestamp=new Date().getTime();

  • FIREFOX 下载中文文件名出现乱码的java解决方案
if  ( "FF". equals (getBrowser (request ) ) )  { // 兼容火狐浏览器-保证中文名不乱码
fileName =  new  String ( "xxxFileName.xlsx". getBytes ( "UTF-8" )"iso-8859-1" ) ;
.......
}
// 服务器端判断客户端浏览器类型
private  static  String getBrowser (HttpServletRequest request )  {
String UserAgent  = request. getHeader ( "USER-AGENT" ). toLowerCase ( ) ;
if  (UserAgent  !=  null )  {
if  (UserAgent. indexOf ( "msie" )  >=  0 )
return  "IE" ;
if  (UserAgent. indexOf ( "firefox" )  >=  0 )
return  "FF" ;
if  (UserAgent. indexOf ( "safari" )  >=  0 )
return  "SF" ;
}
return  null ;
}
  • mysql循环插入1W数据,做导出压测

解决方案:新建mysql存储过程
步骤如下,在navicat中打开数据库test,[函数]->右键选择[过程]->[完成]
复制如下代码-[保存](其中insert换成自己的),名字取multiInsert->在[函数]中找到multiInsert右键->运行函数,坐等十来分钟左右就可以了

BEGIN
DECLARE i  INT  DEFAULT  1 ;
WHILE (i < 10000 )
DO
SET i =i + 1;
SET @mySql = 'INSERT INTO test(id,name) VALUES(1,2)';
PREPARE stmt  FROM @mySql;
EXECUTE stmt;
DEALLOCATE  PREPARE stmt;
END WHILE;
END;
  • JVM常用配置参数

简单来说堆就是Java代码可及的内存,是留给开发人员使用的;
非堆就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、
每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
堆内存分配 JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
(因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。 )
非堆内存分配 JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4
用于存储java永久生成对象(Permanate generation)如,class对象、方法对象这些可反射(reflective)对象
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。
如果程序引用了大量的第三方jar,其大小超过了服务器jvm默认的大小,那么就会产生内存益出问题了。

-server 以服务模式启动jar包,启动比client慢,但可获得更高的运行性能
【堆内存配置】
-Xmx1024m:设置JVM最大可用内存为1024M(缺省值为)
-Xms1024m:设置JVM初始内存为1024M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
【非堆内存配置】
-XX:MaxPermSize=128M :最小尺寸,初始分配
-XX:PermSize=128M :最大允许分配尺寸,按需分配
另外:如果有一个双核的CPU,也许可以尝试这个参数:-XX:+UseParallelGC 让GC可以更快的执行。

你可能感兴趣的:(有模有样)