后端——问题小书(2022)
rehoni / 2022-12-31
1、spring cloud stream介绍
Spring Cloud Stream是一个用来为微服务应用构建消息驱动能力的框架。它可以基于Spring Boot来创建独立的、可用于生产的Spring应用程序。它通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动的微服务应用。Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并且引入了发布-订阅、消费组以及消息分区这三个核心概念。简单的说,Spring Cloud Stream本质上就是整合了Spring Boot和Spring Integration,实现了一套轻量级的消息驱动的微服务框架。通过使用Spring Cloud Stream,可以有效地简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。
Binder绑定器
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
发布-订阅模式
在Spring Cloud Stream中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的Topic主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。这里所提到的Topic主题是Spring Cloud Stream中的一个抽象概念,用来代表发布共享消息给消费者的地方。在不同的消息中间件中,Topic可能对应着不同的概念,比如:在RabbitMQ中的它对应了Exchange、而在Kakfa中则对应了Kafka中的Topic。
消费组
虽然Spring Cloud Stream通过发布-订阅模式将消息生产者与消费者做了很好的解耦,基于相同主题的消费者可以轻松的进行扩展,但是这些扩展都是针对不同的应用实例而言的,在现实的微服务架构中,我们每一个微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例。很多情况下,消息生产者发送消息给某个具体微服务时,只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念。
如果在同一个主题上的应用需要启动多个实例的时候,我们可以通过spring.cloud.stream.bindings.input.group属性为应用指定一个组名,这样这个应用的多个实例在接收到消息的时候,只会有一个成员真正的收到消息并进行处理。
消息分区
通过引入消费组的概念,我们已经能够在多实例的情况下,保障每个消息只被组内一个实例进行消费。通过上面对消费组参数设置后的实验,我们可以观察到,消费组并无法控制消息具体被哪个实例消费。也就是说,对于同一条消息,它多次到达之后可能是由不同的实例进行消费的。但是对于一些业务场景,就需要对于一些具有相同特征的消息每次都可以被同一个消费实例处理,比如:一些用于监控服务,为了统计某段时间内消息生产者发送的报告内容,监控服务需要在自身内容聚合这些数据,那么消息生产者可以为消息增加一个固有的特征ID来进行分区,使得拥有这些ID的消息每次都能被发送到一个特定的实例上实现累计统计的效果,否则这些数据就会分散到各个不同的节点导致监控结果不一致的情况。而分区概念的引入就是为了解决这样的问题:当生产者将消息数据发送给多个消费者实例时,保证拥有共同特征的消息数据始终是由同一个消费者实例接收和处理。
2、mapstruct简单使用
MapStruct 是一个代码生成器,主要用于 Java Bean 之间的映射,如 entity 到 DTO 的映射。
参考
导入 Maven 依赖以及插件
注意lombok和MapStruct 的冲突(在maven install时候会出现属性找不到错误):
- 确保 Lombok 最低版本为 1.18.16
- annotationProcessorPaths 中,要配置lombok
如未使用 lombok 可以去除后两个注解处理器
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.muzaijian.mall</groupId>
<artifactId>mapstruct</artifactId>
<version>1.0-RELEASE</version>
<name>mapstruct</name>
<description>Map Struct project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mapstruct.version>1.4.1.Final</mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- SpringBoot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- MapStruct domain 映射工具 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot 插件,可以把应用打包为可执行 Jar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Maven 编译插件,提供给 MapStruct 使用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<!-- MapStruct 注解处理器 -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- Lombok 注解处理器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- MapStruct 和 Lombok 注解绑定处理器 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写转换接口
package cn.muzaijian.mall.mapstruct.convert;
import cn.muzaijian.mall.mapstruct.domain.dto.PmsBrandItemDTO;
import cn.muzaijian.mall.mapstruct.mbg.entity.PmsBrand;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* <p>
* 商品品牌 转换 Demo One
* </p>
*
* @author muzaijian
* @date 2021/7/12
*/
@Mapper(componentModel = "spring")
public interface PmsBrandConvertDemoOne {
/**
* 商品品牌 ItemDTO 转换商品品牌 entity
*
* @param brandItemDTO 商品品牌 ItemDTO
* @return 商品品牌 entity
*/
@Mapping(source = "bigPicture", target = "bigPic")
PmsBrand convert(PmsBrandItemDTO brandItemDTO);
}
- 需结合 Spring 使用
- domain 实例变量不一样可以使用 @Mapping 注解指定实例变量映射名称
package cn.muzaijian.mall.mapstruct.convert;
import cn.muzaijian.mall.mapstruct.domain.dto.PmsBrandItemDTO;
import cn.muzaijian.mall.mapstruct.mbg.entity.PmsBrand;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* <p>
* 商品品牌 转换 Demo Two
* </p>
*
* @author muzaijian
* @date 2021/7/13
*/
@Mapper
public interface PmsBrandConvertDemoTwo {
PmsBrandConvertDemoTwo INSTANCE = Mappers.getMapper(PmsBrandConvertDemoTwo.class);
/**
* 商品品牌 ItemDTO 转换商品品牌 entity
*
* @param brandItemDTO 商品品牌 ItemDTO
* @return 商品品牌 entity
*/
@Mapping(source = "bigPicture", target = "bigPic")
PmsBrand convert(PmsBrandItemDTO brandItemDTO);
}
- 查看编译后的实现类,可见两者区别只是 PmsBrandConvertDemoOneImpl 多了一个 @Component 注解,可以直接使用依赖注入方式调用
spring注入用法
/**
* description convertBeanList 转化台账json dto为实体库Java bean <br>
* version 1.0 <br>
*
* @param sourceObjects
* @param target
* @return java.util.List<java.lang.Object>
* @date 2022/6/15 10:40 <br>
* @author 罗皓 <br>
*/
private List<Object> convertBeanList(List<Object> sourceObjects, String target) {
String convertMapperClassName = target + "ConvertMapperImpl";
Object beanService = SpringUtil.getBean(convertMapperClassName);
List<Object> targetObjects = new ArrayList<>(sourceObjects.size());
for (Object sourceObject : sourceObjects) {
Object convert = ReflectUtil.invoke(beanService, "convert", sourceObject);
targetObjects.add(convert);
}
return targetObjects;
}
- 编译后的实现类,为在编写的接口类后添加
Impl
作为实现,例如 xxxConvertMapperImpl 为 xxxConvertMapper 的编译实现类。由于使用了@Mapper(componentModel = "spring")
,故可以用SpringUtil获取容器中注入的该接口。
List转化
@Mapping(target = "dydj", source = "voltageLevel")
@Mapping(target = "eddl", source = "ratedCurrent")
Mx convert(PmsBusbarDto pmsBusbarDto);
List<Mx> convertList(List<PmsBusbarDto> list);
3、java单个方法返回多个返回值
可使用hutool的pair,tuple
Pair<List<Object>, List<Object>> pair = convertBeanList(sourceObjects, target);
Tuple tuple = entityToMapAndBytes(RecognitionEntity recognitionEntity);
4、open-feign使用和接口调用
参考
OpenFeign:Spring Cloud声明式服务调用组件(非常详细)
简单使用
POM文件引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
开启配置
@EnableFeignClients
@Slf4j
public class UkApplication {
public static void main(String[] args) {
log.info("########## app is running ##########");
SpringApplication.run(UkApplication.class, args);
}
}
调用feign接口,注意调用服务的context-path需要在url上补上,否则调用接口时会404 Not Found(这也是下边video-service写了两遍的原因)
// 普通POST类型接口
@FeignClient(value = "video-service")
public interface VideoAlarmService {
@PostMapping(value = "video-service/alarm/recognition/create")
Result insertRecognitionResult(@RequestBody RecognitionEntity recognitionEntity);
}
import org.springframework.http.MediaType;
// 上传文件类型接口
@FeignClient(value = "img-upload-service")
public interface UploadService {
@PostMapping(value = "img-upload-service/103-img/v2/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result handleFileUpload(@RequestPart(value = "files") List<MultipartFile> files);
}
// application/x-www-form-urlencode 接口
@PostMapping(value = "auth-service/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
AuthClientResult token(@RequestParam Map<String, Object> params);
// 带header的情况
设置header的5种方式
在微服务间使用Feign进行远程调用时需要在 header 中添加信息,那么 springcloud open feign 如何设置 header 呢?有5种方式可以设置请求头信息:
- 在@RequestMapping注解里添加headers属性
- 在方法参数前面添加@RequestHeader注解
- 在方法或者类上添加@Headers的注解
- 在方法参数前面添加@HeaderMap注解
- 实现RequestInterceptor接口
5、普通maven项目(非spring boot项目)打包成jar包
强烈建议参照官方文档而非搜索教程。https://maven.apache.org/plugins/maven-assembly-plugin/usage.html
1、引入依赖
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<scope>provided</scope>
</dependency>
2、引入插件修改配置
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.nrec.App</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
3、执行打包
第二步直接进行packge,此时会先打一个未整合在一起的jar包,再打jar-with-dependencies,直接进行第二步,打包会产生警告,[WARNING] Cannot include project artifact: com.nrec:envBuild:jar:1.0-SNAPSHOT; it doesn’t have an associated file or directory.,运行jar包导致报错:找不到主类。
6、netty,channelRead0和channelRead的区别【todo】
7、netty中的handler类,通过@Autowired注入的类显示为Null
主要问题在于:Spring Bean的生命周期。
Netty中的handler类,通过@Autowired注入的类显示为Null
原因:netty中无法使用注入的bean,需要主动通过getBean的方式来获取。并不是配置的问题,而是因为netty启动的时候并没有交给spring IOC托管。
1、使用@PostConstruct注解
方法上加该注解会在项目启动的时候执行该方法,即spring容器初始化的时候执行,它与构造函数及@Autowired的执行顺序为:构造函数 » @Autowired » @PostConstruct。
我们想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于注入的bean,那么就无法在构造函数中实现,为此可以使用@PostConstruct注解一个init方法来完成初始化,该方法会在bean注入完成后被自动调用。
@Autowired
ScriptService scriptService;
public static MqttSeverHandler mqttSeverHandler;
@PostConstruct
public void init(){
mqttSeverHandler = this;
mqttSeverHandler.scriptService = this.scriptService;
}
或者如
@ChannelHandler.Sharable
@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Autowired
private ISocketServerService socketServerService;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private static NettyServerHandler nettyServerHandler;
@PostConstruct
public void init() {
nettyServerHandler = this;
}
...
}
2、使用SpringUtil
可以使用hutool的springUtil替代
package com.example.Util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtils.applicationContext == null){
SpringUtils.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext(){
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
使用
@Slf4j
@RequiredArgsConstructor
@Service
@ChannelHandler.Sharable
public class MqttSeverHandler extends SimpleChannelInboundHandler<MqttMessage> {
public static Map<ChannelHandlerContext, JSONObject> sublist=new HashMap<>();
public static Map<String,List<ChannelHandlerContext>> topiclist =new HashMap<>();
public static MqttSeverHandler mqttSeverHandler;
private static ScriptService scriptService;
static {
scriptService = SpringUtils.getBean(ScriptServiceImpl.class);
}
8、netty ByteBuf与String相互转换
String转为ByteBuf
1)使用String.getBytes(Charset),将String转为byte[]类型
2)使用Unpooled.wrappedBuffer(byte[]),将byte[]转为ByteBuf
String msg = "A message";
byte[] bytes = msg.getBytes(CharsetUtil.UTF_8);
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
或者使用 Unpooled.copiedBuffer(CharSequence string, Charset charset)
ByteBuf buf = Unpooled.copiedBuffer("Hello", CharsetUtil.UTF_8);
ByteBuf转为String
使用ByteBuf.toString(Charset),将ByteBuf转为String
buf.toString(CharsetUtil.UTF_8)
示例:
String msg = "A message";
byte[] bytes = msg.getBytes(CharsetUtil.UTF_8);
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
System.out.println(buf.toString(CharsetUtil.UTF_8));
输出:A message
9、System.arraycopy的使用方法详解
System.arraycopy这个方法之前用得很少,前段时间在一个项目需要对很多字节的处理,使用这个方法是非常有用的。 这个方法的作用大家应该都是知道的吧:就是把一个数组中某一段字节数据放到另一个数组中。至于从第一个数组中取出几个数据,放到第二个数组中的什么位置都是可以通知这个方法的参数控制的。
一.System.arraycopy使用的基本定义
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度.
注意:src 和 dest都必须是同类型或者可以进行转换类型的数组.
二.示例详解
System.arraycopy(int[] arr, int star,int[] arr2, int start2, length);
第一个参数是要被复制的数组
第二个参数是被复制的数字开始复制的下标
第三个参数是目标数组,也就是要把数据放进来的数组
第四个参数是从目标数据第几个下标开始放入数据
第五个参数表示从被复制的数组中拿几个数值放到目标数组中
比如:
数组1:int[] arr = { 1, 2, 3, 4, 5 };
数组2:int[] arr2 = { 5, 6,7, 8, 9 };
运行:System.arraycopy(arr, 1, arr2, 0, 3);
得到:
int[] arr2 = { 2, 3, 4, 8, 9 };
过程分析:
先看第1、2、5个参数,得出要从arr中从下标为1的数组中拿出三个数值:2,3,4。然后看第3、4个参数,知道要在arr2中从下标为0开始放入数据,放入的个数也是第五个参加决定的这里是3个,所有最后的结果就是:2,3,4(加入的) + 8,9(原来的)
比如:System.arraycopy(arr , 1 , arr2 , 2 , 3)。表示的是从数组arr中下标为1的位置取出3个数据,放到数组arr2中从下标为2的位置,放入3个数据。
10、@RequestParam和@RequestPart的区别
@RequestPart
@RequestPart
这个注解用在multipart/form-data
表单提交请求的方法上。- 支持的请求方法的方式
MultipartFile
,属于Spring的MultipartResolver
类。这个请求是通过http协议
传输的
@RequestParam
@RequestParam
支持’application/json’,也同样支持multipart/form-data
请求
区别
- 当请求方法的请求参数类型不是String 或 MultipartFile / Part时,而是复杂的请求域时,@RequestParam 依赖Converter or PropertyEditor进行数据解析, RequestPart参考 ‘Content-Type’ header,依赖HttpMessageConverters 进行数据解析
- 当请求为
multipart/form-data
时,@RequestParam
只能接收String类型
的name-value
值,@RequestPart
可以接收复杂的请求域(像json、xml
);@RequestParam
依赖Converter or PropertyEditor
进行数据解析,@RequestPart
参考'Content-Type' header
,依赖HttpMessageConverters
进行数据解析
前台请求:
jsonData
为Person
对象的json
字符串,uploadFile
为上传的图片
后台接收:
@RequestPart
可以将jsonData
的json数据
转换为Person对象
@RequestMapping("jsonDataAndUploadFile")
@ResponseBody
public String jsonDataAndUploadFile(@RequestPart("uploadFile") MultiPartFile uploadFile,
@RequestPart("jsonData") Person person) {
StringBuilder sb = new StringBuilder();
sb.append(uploadFile.getOriginalFilename()).append(";;;"));
return person.toString() + ":::" + sb.toString();
}
@RequestParam
对于jsonData
的json数据
只能用String字符串
来接收
@RequestMapping("jsonDataAndUploadFile")
@ResponseBody
public String jsonDataAndUploadFile(@RequestPart("uploadFile") MultiPartFile uploadFile,
@RequestParam("jsonData") String jsonData) {
StringBuilder sb = new StringBuilder();
sb.append(uploadFile.getOriginalFilename()).append(";;;"));
return person.toString() + ":::" + sb.toString();
}
总结
当请求头中指定Content-Type:multipart/form-data时,传递的json参数,@RequestPart注解可以用对象来接收,@RequestParam只能用字符串接收
11、File文件流转为MultipartFile流
需要借助apache.commons包中的文件类,进行默认配置。不推荐使用springboot-test包(打包后不会引入)。
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
/**
* description getMulFileByFile 根据File构造MultipartFile对象 <br>
* version 1.0 <br>
*
* @param file
* @return org.springframework.web.multipart.MultipartFile
* @date 2022/6/21 15:36 <br>
* @author 罗皓 <br>
*/
private MultipartFile getMulFileByFile(File file) {
FileItem fileItem = createFileItem(file.getPath(), file.getName());
return new CommonsMultipartFile(fileItem);
}
/**
* description createFileItem 创建用于构造CommonsMultipartFile的FileItem对象 <br>
* version 1.0 <br>
*
* @param filePath
* @param fileName
* @return org.apache.commons.fileupload.FileItem
* @date 2022/6/21 15:36 <br>
* @author 罗皓 <br>
*/
private FileItem createFileItem(String filePath, String fileName) {
String fieldName = "file";
FileItemFactory factory = new DiskFileItemFactory(16, null);
FileItem item = factory.createItem(fieldName, "text/plain", false, fileName);
File newfile = new File(filePath);
int bytesRead;
byte[] buffer = new byte[8192];
try {
FileInputStream fis = new FileInputStream(newfile);
OutputStream os = item.getOutputStream();
while ((bytesRead = fis.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return item;
}
12、Mybatis-Plus 新增获取自增列id
1、实体类定义
注意:@TableId(value = “id”, type = IdType.AUTO)注解中的 type = IdType.AUTO
属性标注主键为自增策略。
import lombok.Data;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
@Data
@TableName("users")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("`name`")
private String name;
}
2、解决办法
方法一:使用框架自带的insert方法。
int insert(T entity);
方法二:
@Insert("insert into users(`name`) values(#{user.name})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
Integer add(@Param("user") User user);
方法三:
@InsertProvider(type = UserMapperProvider.class, method = "add")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
Integer add(@Param("user") User user);
UserMapperProvider类
public class UserMapperProvider {
public String add(User user) {
return "insert into users(id, `name`) values(#{user.id},#{user.name})";
}
}
3、调用方法获取id说明:
方法调用前:id为null
方法调用后:id会被赋值
13、@Autowired和@Resource的区别
@Autowired功能虽说非常强大,但是也有些不足之处。比如它跟Spring强耦合了,如果换成了其他框架,功能就会失效。而@Resource是JSR-250提供的,它是Java标准,绝大部分框架都支持。
除此之外,有些场景使用@Autowired无法满足的要求,改成@Resource却能解决问题。
1、@Autowired默认按byType自动装配,而@Resource默认byName自动装配。
2、@Autowired只包含一个参数:required,表示是否开启自动准入,默认是true。而@Resource包含七个参数,其中最重要的两个参数是:name 和 type。
3、@Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。
4、@Autowired能够用在:构造器、方法、参数、成员变量和注解上,而@Resource能用在:类、成员变量和方法上。
5、@Autowired是Spring定义的注解,而@Resource是JSR-250定义的注解。
6、二者装配顺序不同
@Autowired
@Resource
参考
@Autowired和@Resource的区别 - 掘金 (juejin.cn)
14、Netty注解扫盲
@Sharable
- 标识 handler 提醒可共享;
- 没有被此注解标注的 handler 不能重复安装到 pipeline 中,如果没有被该注解的 handler 安装到多个 pipeline 中,那么只有第一个请求的 Client 可以连接成功,之后的 Client 无法连接,并且在 Server 端会报错;
@Skip
- 跳过 handler 的执行;
- 在 Netty 5 中是可以使用的,但是在 Netty 4 中不可以使用;
- 在自己的基于 Netty 4 项目中,如果不想用某个 handler,就直接删除,而不是用 @Skip 注解;
@UnstableApi
- 提醒不稳定,慎用;
@SupressJava6Requirement
- 去除 “Java6 需求”的报警;
- 如果我们基于 Netty 开发的项目要运行在 Java6 上,那么 Java6 以上版本的 API 就不能使用,我们需要先扫描出项目中所有使用到 Java6 以上 API 的地方,可以用一个 Maven 插件:animal-sniffer;
- 不过我们的代码一般会在用到某个版本的 JDK 的 API 之前先用
PlatformDependeng.javaVersion() >= 7
这样的代码判断一下,但是插件 animal-sniffer 会把这样的代码也扫描出来,那么对于这样的代码,就可以使用 @SupressJava6Requirement 一直 animal-sniffer 的行为;
15、Spring Boot配置静态资源的地址与访问路径
静态资源,例如HTML文件、JS文件,设计到的Spring Boot配置有两项,一是“spring.mvc.static-path-pattern”,一是“spring.resources.static-locations”,很多人都难以分辨它们之间的差异,所以经常出现的结果就是404错误,无法找到静态资源。
1. “spring.mvc.static-path-pattern”
spring.mvc.static-path-pattern代表的含义是我们应该以什么样的路径来访问静态资源,换句话说,只有静态资源满足什么样的匹配条件,Spring Boot才会处理静态资源请求,以官方配置为例:
# 这表示只有静态资源的访问路径为/resources/**时,才会处理请求
spring.mvc.static-path-pattern=/resources/**,
假定采用默认的配置端口,那么只有请求地址类似于“http://localhost:8080/resources/jquery.js”时,Spring Boot才会处理此请求,处理方式是将根据模式匹配后的文件名查找本地文件,那么应该在什么地方查找本地文件呢?这就是“spring.resources.static-locations”的作用了。
2. “spring.resources.static-locations”
“spring.resources.static-locations”用于告诉Spring Boot应该在何处查找静态资源文件,这是一个列表性的配置,查找文件时会依赖于配置的先后顺序依次进行,默认的官方配置如下:
spring.resources.static-locations=classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources
继续以上面的请求地址为例,“http://localhost:8080/resources/jquery.js”就会在上述的四个路径中依次查找是否存在“jquery.js”文件,如果找到了,则返回此文件,否则返回404错误。
3. 静态资源的Bean配置
从上面可以看出,“spring.mvc.static-path-pattern”与“spring.resources.static-locations”组合起来演绎了nginx的映射配置,如果熟悉Spring MVC,那么理解起来更加简单,它们的作用可以用Bean配置表示,如下:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public-resources/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
}
}
或者等同与以下的XML。
<mvc:resources mapping="/resources/**" location="/public-resources/">
<mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>123
结论
“spring.mvc.static-path-pattern”用于阐述HTTP请求地址,而“spring.resources.static-locations”则用于描述静态资源的存放位置。
16、Java执行js代码
1、使用ScriptEngineManager
String str = "(msgCategory.equals(\"工况类\") && msgSource.equals(\"告警\"))||(msgCategory.equals(\"事故类\") && msgSource.equals(\"缺陷\"))";
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine js = scriptEngineManager.getEngineByName("js");
js.put("msgCategory", "工况类");
js.put("msgSource", "告警");
Object eval = js.eval(str);
System.out.println("结果类型:" + eval.getClass().getName() + ",计算结果:" + eval);
2、
17、@SuppressWarnings(“deprecation”)
表示不检测过期的方y法,不显示使用了不赞成使用的类或方法时的警告
18、springboot task动态定时任务
1、需要存数据库,修改后,需要等任务下次执行后,再读库生效
数据库中有做简单的cron配置
package com.nrec.pcs9000.app.task;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.nrec.pcs9000.app.entity.GlobalCfg;
import com.nrec.pcs9000.app.entity.OpsDefect;
import com.nrec.pcs9000.app.service.IGlobalCfgService;
import com.nrec.pcs9000.app.service.IOpsDefectService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
/**
* 类 DynamicScheduledTask<code>Doc</code>用于:动态定时任务配置
*
* @author 罗皓
* @version 1.0
* @date 2022/9/5 16:35
*/
@Component
@Slf4j
public class DynamicScheduledTask implements SchedulingConfigurer {
// 每天零点
private static final String DEFAULT_CRON = "0 * * * * ?";
@Resource
private IOpsDefectService opsDefectService;
@Resource
private IGlobalCfgService globalCfgService;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(opsLogTask());
taskRegistrar.addTriggerTask(opsDefectTask());
}
private TriggerTask opsLogTask() {
return new TriggerTask(
//1.添加任务内容(Runnable)
() -> {
log.info("监控日志推送中台");
},
//2.设置执行周期(Trigger)
triggerContext -> {
//2.1 从数据库获取执行周期
String cron = globalCfgService.lambdaQuery()
.eq(GlobalCfg::getPKey, "ops-log.mid.cron")
.oneOpt()
.map(GlobalCfg::getPValue)
.map(str -> getCron(str, "ss mm HH * * ?"))
.orElse(DEFAULT_CRON);
//2.2 合法性校验.
//2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
private TriggerTask opsDefectTask() {
return new TriggerTask(
//1.添加任务内容(Runnable)
() -> {
log.info("执行缺陷推送中台");
opsDefectService.lambdaQuery()
.in(OpsDefect::getState, 3, 4)
.eq(OpsDefect::getIsSend, 0)
.list()
.forEach(opsDefectService::sendMid);
},
//2.设置执行周期(Trigger)
triggerContext -> {
//2.1 从数据库获取执行周期
String cron = globalCfgService.lambdaQuery()
.eq(GlobalCfg::getPKey, "ops-defect.mid.cron")
.oneOpt()
.map(GlobalCfg::getPValue)
.map(str -> getCron(str, "ss mm HH * * ?"))
.orElse(DEFAULT_CRON);
//2.2 合法性校验.
//2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
private String getCron(String dateStr, String format) {
DateTime date = DateUtil.parse(dateStr);
return DateUtil.format(date, format);
}
private String getCron(Date date, String format) {
// "ss mm HH dd MM ? yyyy"
return DateUtil.format(date, format);
}
}
2、使用自定义线程池和Map来存放,可以控制定时任务的启停
数据库中有做简单的cron配置
3、使用自定义线程池和Map来存放,可以控制定时任务的启停
数据库中有做Task实体的存储
- serviceImpl实现
package com.nrec.pcs9000.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.nrec.base.common.model.Result;
import com.nrec.pcs9000.app.entity.ScheduleTask;
import com.nrec.pcs9000.app.mapper.ScheduleTaskMapper;
import com.nrec.pcs9000.app.schedule.TaskRunnable;
import com.nrec.pcs9000.app.service.IScheduleTaskService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* 动态定时任务 服务实现类
*
* @author liucq
* @since 2021-11-11
*/
@Slf4j
@Service
public class ScheduleTaskServiceImpl extends ServiceImpl<ScheduleTaskMapper, ScheduleTask>
implements IScheduleTaskService, CommandLineRunner {
@Resource
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Resource
private ScheduleTaskMapper scheduleTaskMapper;
//存放任务调度的容器,此容器中的任务都是正在运行的任务
private ConcurrentHashMap<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>();
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(10);
return threadPoolTaskScheduler;
}
private ScheduleTask getScheduleTaskById(String id) {
return baseMapper.getScheduleTaskById(id);
}
@Override
public Result start(String jobIds) {
String[] jobs = jobIds.split(",");
for (String jobId : jobs) {
ScheduleTask task = scheduleTaskMapper.getScheduleTaskById(jobId);
if (task == null) {
return Result.buildFailed(jobId + " 任务不存在,无法启动!");
}
if (futureMap.get(jobId) != null) {
return Result.buildFailed("任务已经在运行,无法重复启动!");
}
ScheduledFuture future = threadPoolTaskScheduler.schedule(new TaskRunnable(task), new CronTrigger(task.getCron()));
baseMapper.updateEnableStart(jobId, 1);
//放入任务调度集合中
assert future != null;
futureMap.put(jobId, future);
}
return Result.buildSuccess(jobIds + " start success", "通过任务id启动任务");
}
@Override
public Result stop(String jobIds) {
String[] jobs = jobIds.split(",");
for (String jobId : jobs) {
pause(jobId);
scheduleTaskMapper.updateEnableStart(jobId, 0);
}
return Result.buildSuccess(jobIds + "停止程序自启成功", "通过任务id停止任务并取消随程序启动");
}
@Override
public Result edit(String jobId, String cron) {
scheduleTaskMapper.updateCron(jobId, cron);
ScheduledFuture future = futureMap.get(jobId);
if (null != future) {
pause(jobId);
start(jobId);
return Result.buildSuccess(jobId + " edit success", "通过任务id编辑任务执行周期");
}
return Result.buildFailed(jobId + " edit failed");
}
@Override
public List<ScheduleTask> listRunningTask() {
List<ScheduleTask> tasks = new ArrayList<>();
futureMap.forEach((k, v) -> {
tasks.add(getScheduleTaskById(k));
});
return tasks;
}
@Override
public List<ScheduleTask> listAllTask() {
return list();
}
@Override
public boolean startAllScheduleTask() {
QueryWrapper<ScheduleTask> wrapper = new QueryWrapper<>();
wrapper.eq("DEFAULT_START", true);
List<ScheduleTask> scheduleTasks = scheduleTaskMapper.selectList(wrapper);
scheduleTasks.forEach(task -> {
start(task.getJobId());
});
return true;
}
@Override
public Result<String> pause(String id) {
ScheduledFuture future = futureMap.get(id);
if (future != null) {
//从调度集合中移除
future.cancel(true);
futureMap.remove(id);
return Result.buildSuccess("定时任务暂停成功", "通过任务id暂停任务并不会取消随程序启动");
}
return Result.buildFailed("该任务不在运行行列中,无法暂停!");
}
/**
* 程序启动时执行启动所有定时任务过程
*
* @param args
* @throws Exception
*/
@Override
public void run(String... args) throws Exception {
startAllScheduleTask();
}
}
- controller实现
package com.nrec.pcs9000.app.controller;
import com.nrec.base.common.model.*;
import com.nrec.pcs9000.app.service.IScheduleTaskService;
import com.nrec.pcs9000.app.entity.ScheduleTask;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import javax.annotation.Resource;
import java.util.List;
import static com.alibaba.fastjson.JSON.toJSONString;
/**
* 动态定时任务 前端控制器
*
* @author liucq
* @since 2021-11-11
*/
@RestController
@Slf4j
@Api(tags = "动态定时任务模块")
@RequestMapping("")
public class ScheduleTaskController {
@Resource
public IScheduleTaskService scheduleTaskService;
@ApiOperation(value = "通过任务id停止任务并取消随程序启动")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "任务id", required = true, paramType = "path")
})
@GetMapping(value = "/schedule-task/stop/{ids}/update")
public Result<String> stopScheduleTaskById(@PathVariable(name = "ids") String ids) {
return scheduleTaskService.stop(ids);
}
// @ApiOperation(value = "通过任务id暂停任务不会取消随程序启动")
// @ApiImplicitParams({
// @ApiImplicitParam(name = "id", value = "任务id", required = true, paramType = "path")
// })
// @GetMapping(value = "/schedule-task/pause/{id}/update")
// public Result<String> pauseScheduleTaskById(@PathVariable(name = "id") String id) {
// return scheduleTaskService.pause(id);
// }
@ApiOperation(value = "通过任务id编辑任务执行周期")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "任务id", required = true, paramType = "path")
})
@GetMapping(value = "/schedule-task/edit/{id}/update")
public Result<String> editScheduleTaskById(@PathVariable(name = "id") String id, @RequestParam("cron") String cron) {
return scheduleTaskService.edit(id, cron);
}
@ApiOperation(value = "通过任务id启动任务")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "任务id", required = true, paramType = "path")
})
@GetMapping(value = "/schedule-task/start/{ids}/update")
public Result<String> startScheduleTaskById(@PathVariable(name = "ids") String ids) {
return scheduleTaskService.start(ids);
}
@ApiOperation(value = "查询所有正在执行任务及执行周期")
@GetMapping(value = "/schedule-task/list/run/access")
public Result<List<ScheduleTask>> getRunningTask() {
return Result.buildSuccess(scheduleTaskService.listRunningTask(),"查询所有正在执行任务及执行周期");
}
@ApiOperation(value = "查询所有任务及执行周期")
@GetMapping(value = "/schedule-task/list/all/access")
public Result<List<ScheduleTask>> getAllTask() {
return Result.buildSuccess(scheduleTaskService.listAllTask(),"查询所有任务及执行周期");
}
}
19、hutool实现属性字段映射
A实体,从B、C实体拷贝。
A.emsFieldInB = B.fieldInB
A.midFieldInC = B.fieldInC
BeanUtil.copyProperties(asset, adLedgerDto, CopyOptions.create().setFieldNameEditor(sourceEditor("mid")));
private Editor<String> sourceEditor(String prefix) {
return fieldName -> prefix + CharSequenceUtil.upperFirst(fieldName);
}
20、oval使用
总结说明针对不同场景的主要使用那些注解。
- 字符类型
@AsserURL、@Email、@Length、@MaxLength、@MinLength
@NotNull、@NotBlank、@NotEmpty、
@Digits、@HasSubstring
- 数值类型
@Range、@Max、@Min、@NotNegative
- 布尔类型
@AssertFalse、@AssertTrue
- 集合数组
@Size、@MaxSize、@MinSize、@MemberOf、@NotMemberOf
- 表达式或自定义
@Assert、@CheckWith、@NotMatchPatternCheck,@MatchPatternCheck、
@ValidateWithMethod
可供参考:
java开源验证框架OVAL帮助文档_梦想画家的博客-CSDN博客
Oval框架如何校验枚举类型的一种思路 - mumuxinfei - 博客园 (cnblogs.com)
21、将html转化为PDF
controller
@PostMapping(value = "/preview", produces = "application/octet-stream")
@ApiOperation(value = "打印")
public void preview(@RequestBody ReportQo reportQo,
HttpServletRequest request, HttpServletResponse response) throws IOException {
reportQo.setCurrentPage(1);
reportQo.setPageSize(10000);
ReportDataDto data = this.getReportTableData(reportQo).getData();
// 构造freemarker模板引擎参数,listVars.size()个数对应pdf页数
List<Map<String, Object>> listVars = new ArrayList<>();
Map<String, Object> variables = new HashMap<>();
// variables.put("var1", "xxxxxxx1");
// variables.put("var2", "xxxxxxxxxxxxx2");
listVars.add(variables);
PdfUtils.preview(configurer, "pdfReportTable.ftl", data, listVars, response);
}
@PostMapping(value = "/download", produces = "application/octet-stream")
@ApiOperation(value = "导出")
public void download(@RequestBody ReportQo reportQo,
HttpServletRequest request, HttpServletResponse response) throws IOException {
reportQo.setCurrentPage(1);
reportQo.setPageSize(10000);
ReportDataDto data = this.getReportTableData(reportQo).getData();
List<Map<String, Object>> listVars = new ArrayList<>();
Map<String, Object> variables = new HashMap<>();
// variables.put("title","测试下载ASGX!");
listVars.add(variables);
PdfUtils.download(configurer, "pdfReportTable.ftl", data, listVars, response, "export.pdf");
}
pdfUtils
package com.nrec.pcs9000.app.util;
import com.lowagie.text.pdf.BaseFont;
import com.nrec.pcs9000.app.model.dto.ReportDataDto;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.util.List;
import java.util.Map;
/**
* 功能:pdf处理工具类
*
* @author qust
* @version 1.0 2018/2/23 17:21
*/
public class PdfUtils {
private PdfUtils() {
}
private static final Logger LOGGER = LoggerFactory.getLogger(PdfUtils.class);
/**
* 按模板和参数生成html字符串
*
* @param templateName freemarker模板名称
* @param variables freemarker模板参数
* @return String
* <p>
* 解决打包后Windows下调用出现空指针问题
*/
private static String generateDocString(FreeMarkerConfigurer configurer, String templateName, Object variables) {
StringWriter writer = new StringWriter();
try {
Template template = configurer.getConfiguration().getTemplate(templateName);
template.process(variables, writer);
}
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
writer.flush();
return writer.toString();
}
/**
* 按模板和参数生成html字符串,再转换为flying-saucer识别的Document
*
* @param templateName freemarker模板名称
* @param variables freemarker模板参数
* @return Document
* <p>
* Deprecated暂停使用, 调整为generateDocString()进行生成
*/
@Deprecated
private static Document generateDoc(FreeMarkerConfigurer configurer, String templateName, Map<String, Object> variables) {
Template tp;
try {
tp = configurer.getConfiguration().getTemplate(templateName);
}
catch (IOException e) {
LOGGER.error(e.getMessage(), e);
return null;
}
StringWriter stringWriter = new StringWriter();
try (BufferedWriter writer = new BufferedWriter(stringWriter)) {
try {
tp.process(variables, writer);
writer.flush();
}
catch (TemplateException e) {
LOGGER.error("模板不存在或者路径错误", e);
}
catch (IOException e) {
LOGGER.error("IO异常", e);
}
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes()));
}
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return null;
}
}
/**
* 核心: 根据freemarker模板生成pdf文档
*
* @param configurer freemarker配置
* @param templateName freemarker模板名称
* @param out 输出流
* @param listVars freemarker模板参数
* @throws Exception 模板无法找到、模板语法错误、IO异常
*/
private static void generateAll(FreeMarkerConfigurer configurer, String templateName, OutputStream out, ReportDataDto data, List<Map<String, Object>> listVars) throws Exception {
if (CollectionUtils.isEmpty(listVars)) {
LOGGER.warn("警告:freemarker模板参数为空!");
return;
}
System.setProperty("javax.xml.transform.TransformerFactory", "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
ITextRenderer renderer = new ITextRenderer();
// Document doc = generateDoc(configurer, templateName, listVars.get(0));
// 组装参数
String doc = generateDocString(configurer, templateName, data);
renderer.setDocumentFromString(doc);
//设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
ITextFontResolver fontResolver = (ITextFontResolver) renderer.getSharedContext().getFontResolver();
fontResolver.addFont("fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont("fonts/SourceHanSansSC.otf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// fontResolver.addFont("fonts/HarmonyOS_Sans_SC_Medium.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// fontResolver.addFont("fonts/GB2312.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// fontResolver.addFont("fonts/fzhtjw.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
//展现和输出pdf
renderer.layout();
renderer.createPDF(out, false);
//根据参数集个数循环调用模板,追加到同一个pdf文档中
//(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
for (int i = 1; i < listVars.size(); i++) {
String docAppend = generateDocString(configurer, templateName, listVars.get(i));
renderer.setDocumentFromString(docAppend);
renderer.layout();
renderer.writeNextDocument(); //写下一个pdf页面
}
renderer.finishPDF(); //完成pdf写入
}
/**
* pdf下载
*
* @param configurer freemarker配置
* @param templateName freemarker模板名称(带后缀.ftl)
* @param listVars 模板参数集
* @param response HttpServletResponse
* @param fileName 下载文件名称(带文件扩展名后缀)
*/
public static void download(FreeMarkerConfigurer configurer, String templateName, ReportDataDto data,
List<Map<String, Object>> listVars, HttpServletResponse response, String fileName) {
// 设置编码、文件ContentType类型、文件头、下载文件名
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
try {
response.setHeader("Content-Disposition", "attachment;fileName=" +
new String(fileName.getBytes("gb2312"), "ISO8859-1"));
}
catch (UnsupportedEncodingException e) {
LOGGER.error(e.getMessage(), e);
}
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(configurer, templateName, out, data, listVars);
out.flush();
}
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
/**
* pdf预览
*
* @param configurer freemarker配置
* @param templateName freemarker模板名称(带后缀.ftl)
* @param listVars 模板参数集
* @param response HttpServletResponse
*/
public static void preview(FreeMarkerConfigurer configurer,
String templateName, ReportDataDto data,
List<Map<String, Object>> listVars, HttpServletResponse response) {
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(configurer, templateName, out, data, listVars);
out.flush();
}
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
}
参考:Rehoni/demo-pdf: SpringBoot + FreeMarker + FlyingSaucer 实现pdf在线预览下载 (github.com)
maven打jar包字体出现:fonts/simsun.ttc is not a valid TTF file.
com.lowagie.text.DocumentException: fonts/simsun.ttc is not a valid TTF file.
在pom文件中,将静态fonts字体资源配置如下:注意出现这种问题时,重新打包需要先clean!
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<excludes>
<exclude>fonts/*</exclude>
<exclude>application${application.exclude}.yml</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
<includes>
<include>fonts/*</include>
</includes>
</resource>
</resources>
解决javax.xml.parsers.DocumentBuilderFactory.setFeature(Ljava/lang/String;Z)V异常
java.lang.AbstractMethodError:javax.xml.parsers.DocumentBuilderFactory.setFeature(Ljava/lang/String;Z)
原因:不同jar包的多xml解析器冲突
在使用DocumentBuilderFactory前加入这一行代码,后者的具体是哪个Impl需要根据源码打印的堆栈检索一下,确认引入正确的xml解析器。
System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
flying-saucer + iText + Freemarker生成pdf表格跨页问题
通过样式控制,默认换行
table {
page-break-inside:auto;
}
tr {
page-break-inside:avoid;
page-break-after:auto;
}
22、Freemarker
listMap和split
rows.rows是个 List<Map<String,Object»
<#list hds?split(",") as title >
split的简单使用
(map[key])!" "
进行了判空,null则替换为空格
<div style="display: table; margin: 0 auto;font-size: 8px;">
<table style="width: 186mm">
<tr>
<td>序号</td>
<#list hds?split(",") as title >
<td>${title}</td>
</#list>
</tr>
<#list rows.rows as map>
<tr>
<td>${map["rowId"]}</td>
<#list ids?split(",") as key >
<td>${(map[key])!" "}</td>
</#list>
</tr>
</#list>
</table>
</div>
参考:FreeMarker对应各种数据结构解析 - 掘金 (juejin.cn)
23、负载均衡定时任务加锁
0、表结构,以金仓为例,其他数据库建表语句去官方找
CREATE TABLE SHEDLOCK
(
NAME VARCHAR(64) NOT NULL,
LOCK_UNTIL TIMESTAMP NOT NULL,
LOCKED_AT TIMESTAMP NOT NULL,
LOCKED_BY VARCHAR(255) NOT NULL,
PRIMARY KEY (NAME)
);
1、pom.xml 引入依赖
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.42.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.42.0</version>
</dependency>
2、config 启用
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "1m")
public class ScheduleTaskConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
// .usingDbTime() // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2
.build()
);
}
}
3、定时任务上配置
@Scheduled(fixedDelay = 3000L, initialDelay = 10000L)
@SchedulerLock(name = "smsDistribute", lockAtMostFor = "1m", lockAtLeastFor = "2s")
public void smsDistribute() {
log.info("smsDistribute start, thread name: {} , time: {}.", Thread.currentThread().getName(), DateUtil.now());
// dosomething
log.info("smsDistribute end, thread name: {} , time: {}.", Thread.currentThread().getName(), DateUtil.now());
}
24、Eureka 客户端不注册
# 不向注册中心注册 , 是否注册到服务中心 , false = 不注册,true = 注册
eureka.client.register-with-eureka: false,
# 不获取注册列表信息, 是否从eureka服务器获取注册信息 , false = 不获取,true = 获取
eureka.client.fetch-registry: false
25、spring cloud gateway url重写
将/a/b/c指向到/f/c的控制示例:
spring:
cloud:
gateway:
routes:
# =====================================
- id: rewritepath_route
uri: http://example.org
predicates:
- Path=/a/b/**
filters:
- RewritePath=/a/b/(?<segment>.*), /f/$\{segment}