Luo Hao

后端——问题小书(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 的映射。

参考

优雅的对象转换解决方案-MapStruct使用进阶(二)

MapStruct使用指南

导入 Maven 依赖以及插件

注意lombok和MapStruct 的冲突(在maven install时候会出现属性找不到错误):

  1. 确保 Lombok 最低版本为 1.18.16
  2. 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);

}
  1. 需结合 Spring 使用
  2. 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);
}

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;
    }

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声明式服务调用组件(非常详细)

OpenFeign设置header的5种方式

简单使用

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种方式可以设置请求头信息:

  1. 在@RequestMapping注解里添加headers属性
  2. 在方法参数前面添加@RequestHeader注解
  3. 在方法或者类上添加@Headers的注解
  4. 在方法参数前面添加@HeaderMap注解
  5. 实现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、执行打包

image-20220425152151013

第二步直接进行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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIsyCkP6-1649826757555)(G:\西电研究生\学长简历\项目经验总结.assets\image-20210928173236517.png)]

我们想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于注入的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);

第一个参数是要被复制的数组

第二个参数是被复制的数字开始复制的下标

第三个参数是目标数组,也就是要把数据放进来的数组

第四个参数是从目标数据第几个下标开始放入数据

第五个参数表示从被复制的数组中拿几个数值放到目标数组中

比如:

数组1int[] arr = { 1, 2, 3, 4, 5 };
数组2int[] 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

@RequestParam

区别

前台请求:

jsonDataPerson对象的json字符串,uploadFile为上传的图片

POST

后台接收:

@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();
}
@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为null

方法调用后:id会被赋值

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

@Skip

@UnstableApi

@SupressJava6Requirement

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实体的存储

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();
    }
}
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)

oval api

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}