一、开发分析
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无法访问”);
- statusCodeMap.put(“102”, “该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[]
ByteArrayOutputStream swapStream = new ByteArrayOutputStream ( ) ;
byte [ ] buff = new byte [ 100 ] ;
int rc = 0 ;
while ( (rc = inStream. read (buff, 0, 100 ) ) > 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关闭失败(一般为中途点击了取消)" ) ;
}
}
@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) 注解,显示指定数据源实例
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?
在使用单个mapper时没有报这个警告错误,但是当把mapper增加为2个以上时,就会报该异常信息,不知具体的原因。
跟踪代码,看到会优先执行MyBatisMapperScannerConfig扫描,虽然设置了AutoConfigureAfter,MyBatisMapperScannerConfig修改definition.setBeanClass(MapperFactoryBean.class)后,spring在做dataSourceInitializerPostProcessor处理时,会抛出该异常,不知道哪里出了问题。而且会重复出现2,3边同样的WARN,百分百重现。
<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解决方案
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右键->运行函数,坐等十来分钟左右就可以了
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可以更快的执行。