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

xiaoxiao2021-02-28  101

一、开发分析

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版本及以上就没问题了。 <dependency> <groupId>org.mybatis </groupId> <artifactId>mybatis-spring </artifactId> <version>1.2.5 </version> </dependency>

详细内容请直接到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时,配置了<scope>provided</scope>,去掉该scope即可 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency>

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可以更快的执行。

转载请注明原文地址: https://www.6miu.com/read-25932.html

最新回复(0)