1、如何下载从数据库中查询出来的数据
查询结果List 写到文件中,然后下载
@GetMapping(value = "/download")
public void download(HttpServletResponse response)
throws IOException {
List<ticket> tickets = getTickets();
File tmpFile = write2CSVFile(tickets);
final String fileName = tmpFile.getName();
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition",
"attachment;filename=" + fileName);
FileUtils.readToOutputStream(tmpFile.getPath(),
response.getOutputStream(), 0);
response.getOutputStream().close();
}
FileUtils
/**
* @param filePath path of the file which work as the input source
* @param outputStream the OutputStream to which the data is written
* @param skippedSize the size of the bytes which is skipped for every file
*/
public static OutputStream readToOutputStream(
String filePath, OutputStream outputStream,
long skippedSize) throws IOException {
int bytesRead;
BufferedOutputStream bufferedOutputStream =
new BufferedOutputStream(outputStream);
byte[] buffer = new byte[1024 * 1024];
try (BufferedInputStream inputStream =
new BufferedInputStream(
new FileInputStream(filePath))) {
inputStream.skip(skippedSize);
while ((bytesRead = inputStream.read(buffer)) != -1) {
bufferedOutputStream.write(buffer, 0, bytesRead);
}
}
bufferedOutputStream.flush();
return bufferedOutputStream;
}
2、mybatis plus in 参数个数超过限制报错
mybatis plus in 参数个数限制1000条。
解决办法:
in 大集合 改为 多个in 小集合
即分多个in 用or连接
notIn同理,
notIn分为多个 notIn 用and连接
// run sql
LambdaQueryWrapper<Ticket> lambdaQueryWrapper =
new LambdaQueryWrapper<>();
// mybatis plus in 参数个数超过限制报错
try {
cutInParameter(lambdaQueryWrapper, Ticket::getOneId,
oneIdListWithWhitelist);
} catch (Exception e) {
log.error("cut parameter fail", e);
throw new BusinessException("cut parameter fail");
}
public static <T, F> void cutInParameter(LambdaQueryWrapper<T> wrapper,
SFunction<T, ?> column,
List<F> coll) throws Exception {
List<List<F>> newList = splitList(coll, 900);
if (ObjectUtils.isEmpty(newList)) {
return;
} else if (newList.size() == 1) {
wrapper.notIn(column, newList.get(0));
return;
}
wrapper.and(i -> {
i.notIn(column, newList.get(0));
newList.remove(0);
for (List<F> objects : newList) {
i.and(w -> {
w.notIn(column, objects);
});
}
});
}
public static <T> List<List<T>> splitList(List<T> list, int groupSize) {
int length = list.size();
// 计算可以分成多少组
int num = (length + groupSize - 1) / groupSize;
List<List<T>> splitList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
// 开始位置
int fromIndex = i * groupSize;
// 结束位置
int toIndex = Math.min((i + 1) * groupSize, length);
splitList.add(list.subList(fromIndex, toIndex));
}
return splitList;
}
3、springboot 自定义类加载器
利用springboot的maven插件打包jar,在java -jar运行时,会采用springboot自定义的类加载器来加载类,使用的是LaunchedURLClassLoader,(使用assembly插件不会有这个问题),
但是代码在IDEA中能够运行,但是打包成jar之后无法运行,报ClassCastException
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<includeSystemScope>true</includeSystemScope>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>agent</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>false</attach>
<outputDirectory>${project.basedir}</outputDirectory>
<finalName>${final.name}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>
${assembly.descriptor.dir}/assembly.xml
</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
类的唯一性如何确定:
由加载这个类的类加载器和类本身来唯一确定一个类,也就是说类相同,但是类加载器不同也会被认为是不同的类。
- 类加载器相同,指的是同一个类加载器对象(不同实例也是不同的类加载器),而不是类加载器的类相同
在业务代码中我们可能需要加载外部指定路径的jar,加载jar时我们一版使用java.net.URLClassLoader
,此时如果存在外部jar和springboot加载的类之间有关联,比如有一个类两个类加载器都加载了,那么会被认为是不同的类,由此可能引起ClassCastException
。
在IDEA中能运行的原因是:在IDEA中统一使用了AppClassLoader这个加载器,这个加载器也称为系统类加载器。
每个加载器加载的位置不同。
一般会使用的方式,url类似 file:/path/to/jar
private URLClassLoader loader;
private Properties props = new Properties();
public void loadJar(URL url) throws IOException {
loader = new URLClassLoader(new URL[]{url});
try (InputStream stream = loader
.getResourceAsStream(AdaptorPropsList.HIGHFLIP_PROPERTIES_FILE)) {
if (stream != null) {
props.load(stream);
} else {
log.info("Missed {} property file.", AdaptorPropsList.HIGHFLIP_PROPERTIES_FILE);
}
}
}
springboot 使用LaunchedURLClassLoader来加载类,此时加载外部jar包用java.net.URLClassLoader,这样类加载器不一致,会引发问题,正确的加载类的方式:
private URLClassLoader loader;
private Properties props = new Properties();
public void loadJar(URL url) throws IOException {
if (Thread.currentThread()
.getContextClassLoader() instanceof LaunchedURLClassLoader) {
loader = (LaunchedURLClassLoader) Thread.currentThread()
.getContextClassLoader();
try {
Method addURL =
LaunchedURLClassLoader.class.getSuperclass()
.getDeclaredMethod("addURL",
new Class[] {URL.class});
addURL.setAccessible(true);
addURL.invoke(loader, new Object[] {url});
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
loader = new URLClassLoader(new URL[] {url});
}
try (InputStream stream = loader
.getResourceAsStream("app.properties")) {
if (stream != null) {
props.load(stream);
} else {
log.info("Missed {} property file.", "app.properties");
}
}
}
获取当前线程的类加载器
Thread.currentThread().getContextClassLoader()
LaunchedURLClassLoader类所在的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
LaunchedURLClassLoader部分源码
public class LaunchedURLClassLoader extends URLClassLoader {
private static final int BUFFER_SIZE = 4096;
private final boolean exploded;
private final Archive rootArchive;
private final Object packageLock;
private volatile LaunchedURLClassLoader.DefinePackageCallType definePackageCallType;
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
this(false, urls, parent);
}
public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) {
this(exploded, (Archive)null, urls, parent);
}
public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) {
super(urls, parent);
this.packageLock = new Object();
this.exploded = exploded;
this.rootArchive = rootArchive;
}
...
}
源码中可以看到
- LaunchedURLClassLoader没有提供addURL的方法来加载指定路径的jar包
- LaunchedURLClassLoader继承了URLClassLoader
- URLClassLoader提供了addURL的方法,但是是protected方法,调用不到
为了能调用addURL方法,采用反射机制来调用,先反射拿到LaunchedURLClassLoader.class
类,再getSuperclass()
拿到URLClassLoader的类对象,添加url,完成加载外部指定路径的jar包。
4、mybatis plus打印sql语句
application.yaml中添加配置:
mybatis-plus:
configuration:
### 开启打印sql配置
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
### 开启驼峰配置
map-underscore-to-camel-case: true
5、URL 编码
+为URL编码保留字符,不会被编码,
空格URL编码后为+
http://server.com/a/path?key1=value1&key2=value2
如果value1中的参数有+,这时候 后端收到的会decode将+号解码为空格,
比如value1=aaa+bbb,后端key1得到的值是aaa bbb
后端如何解决这种问题?
value1.replaceAll(" ", "+")
6、获取当前jar的目录
获取当前jar在机器上的目录:
/**
* 获取jar包同目录
*
* @return
*/
private String getJarFilePath() {
ApplicationHome home = new ApplicationHome(getClass());
File jarFile = home.getSource();
return jarFile.getParentFile().toString();
}
注意此方法使用时jar包必须存在,否则会报错。
7 Jackson: java.util.LinkedHashMap cannot be cast to X
Jackson反序列化时转换异常。objectMapper.readValue(),当没有指定类型时,会将object类型反序列化为LinkedHashMap类型
Jackson:
when Jackson attempts to deserialize an object in JSON but no target type information is given, it’ll use the default type: LinkedHashMap.
如何解决这种问题:
反序列化时,传入类型
** we can pass a TypeReference object to the objectMapper.readValue(String content, TypeReference valueTypeRef) method.**
eg:
List<Book> bookList = objectMapper.readValue(jsonString, new TypeReference<List<Book>>() {});