JAVA常用工具方法

1、poi生成EXCEL

//创建工作簿
        HSSFWorkbook workbook=new HSSFWorkbook();
        //--------------------------- 下面创建第一个sheet
        //创建sheet1
        HSSFSheet sheet1= workbook.createSheet("能力详情");
        //创建Excel工作表的行
        HSSFRow row = sheet1.createRow(0);
        //获取表头
        HSSFCell cell=null;
        List<String> heads= Arrays.asList("一","二","三");
        //设置样式
        HSSFCellStyle style=workbook.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        HSSFCellStyle style2=workbook.createCellStyle();
        style2.setAlignment(HorizontalAlignment.CENTER);
        style2.setVerticalAlignment(VerticalAlignment.CENTER);
        HSSFFont font= workbook.createFont();
        font.setFontHeightInPoints((short) 24);//字号
        font.setBold(true);//加粗
        font.setFontName("宋体");//字体
        style.setFont(font);
        for (int i = 0; i < heads.size(); i++) {
            cell= row.createCell(i);//创建一个单元格
            cell.setCellValue(heads.get(i));//设置该单元格内容
            sheet1.setColumnWidth(i,5000);//设置列宽
            cell.setCellStyle(style);//使用样式
        }
        row.setHeight((short) 1000);//设置行高 (根据row的行高)
        //内容填充
        HSSFRow body=null;
        for (int i = 0; i < 5; i++) {
            body=sheet1.createRow(i+1);
            body.setHeight((short)1000);
            cell= body.createCell(0);
            cell.setCellValue("内容1");
            cell.setCellStyle(style2);//使用样式
            cell= body.createCell(1);
            cell.setCellValue("内容2");
            cell.setCellStyle(style2);//使用样式
            cell= body.createCell(2);
            cell.setCellValue("内容3");
            cell.setCellStyle(style2);//使用样式
        }
        //--------------------------- 下面创建第二个sheet
        //创建sheet2
        HSSFSheet sheet2= workbook.createSheet("超10万");
        //创建Excel工作表的行
        HSSFRow sheet2row = sheet2.createRow(0);
        //获取表头
        HSSFCell cell2=null;
        List<String> heads2= Arrays.asList("四","五","六");
        for (int i = 0; i < heads2.size(); i++) {
            cell2= sheet2row.createCell(i);//创建一个单元格
            cell2.setCellValue(heads2.get(i));//设置该单元格内容
        }
        sheet2.setColumnWidth(0,5000);
        sheet2.setColumnWidth(1,5000);
        sheet2.setColumnWidth(2,5000);
        sheet2row.setHeight((short) 1000);
        //内容填充
        HSSFRow body2=null;
        for (int i = 0; i < 5; i++) {
            body2=sheet2.createRow(i+1);
            body2.setHeight((short)1000);
            cell2= body2.createCell(0);
            cell2.setCellValue("内容4");
            cell2= body2.createCell(1);
            cell2.setCellValue("内容5");
            cell2= body2.createCell(2);
            cell2.setCellValue("内容6");
        }


        //(根据服务器指定路径更改)
        String filepath="D:\\"+"文件名.xlsx";
        File file=new File(filepath);
        try {
            //创建新文件
            file.createNewFile();
            //文件输出流
            FileOutputStream stream= FileUtils.openOutputStream(file);
            //清理
            stream.flush();
            //讲内容写入输出流
            workbook.write(stream);
            //关闭
            stream.close();
            //或者
            // HttpServletResponse response
   //          OutputStream output=response.getOutputStream();
			// response.reset();
			// //设置响应头
			// response.setHeader("Content-disposition", "attachment; filename="+ URLEncoder.encode("未命中问题详情","UTF-8")+"("+s+"-"+e+").xls");
			// response.setContentType("application/msexcel");
			// workbook.write(output);
			// output.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

2、邮件发送(jdk 17)

implementation 'org.springframework.boot:spring-boot-starter-mail' //邮件集成
package cn.djshen.ms.util;

import cn.djshen.ms.config.AppConfig;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.Resource;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;

/**
 * @author djshen
 * @description 邮箱发送工具
 * @filename MailUtil
 * @Date 2023/4/27 11:20
 */
@Component
public class MailUtil {
    private static final Logger logger= LoggerFactory.getLogger(MailUtil.class);
    @Resource
    private JavaMailSender mailSender;
    @Value("${spring.mail.username}")
    private String sendMailer;
    @Resource
    private AppConfig appConfig;
    /**
     * 内容 模版
     */
    private static final String commentMailType = "嘿!你在 %s 中收到一条新消息。";
    private static final String loginExceptionMailType = "%s 登录异常提醒!";
    private static final String replyMail = "你之前的评论收到来自 %s 的回复";
    private static final String commentMail = "你的文章 %s 收到来自 %s 的评论";
    private static final String messageMail = "你收到来自 %s 的留言";
    private static final String imMail = "你收到来自 %s 的消息";
    private static final String loginExceptionMail = "你收到 %s 的登录异常消息";
    private static final String textStart = "尊敬的%s:";
    private static final String textStartDefault = "尊敬的用户:";

    /**
     * 背景地址
     */
    private static final String backgroundUrl="https://sdj-yxy.oss-cn-hangzhou.aliyuncs.com/ms/mail/2023/07/Veds2GnYEjZ9G_hzyx-cS.webp";

    /**
     * 邮件内容 模版
     */
    private static final String mailText = """
            <div style="font-family: serif;line-height: 22px;padding: 30px">
                            <div style="display: flex;justify-content: center;width: 100%  ;max-width: 900px;background-image: url('{}');background-size: cover;border-radius: 10px">
                                <div style="margin-top: 20px;display: flex;flex-direction: column;align-items: center">
                                    <div style="margin: 10px auto 20px;text-align: center">
                                        <div style="line-height: 32px;font-size: 26px;font-weight: bold;color: #000000">
                                            {}
                                        </div>
                                        <div style="font-size: 16px;font-weight: bold;color: rgba(0, 0, 0, 0.19);margin-top: 21px">
                                            {}
                                        </div>
                                    </div>
                                    <div style="min-width: 250px;max-width: 800px;min-height: 128px;background: rgba(247, 247, 247, 0.4);border-radius: 10px;padding: 32px">
                                        <div>
                                            <div style="font-size: 18px;font-weight: bold;color: #C5343E">
                                                {}
                                            </div>
                                            <div style="margin-top: 6px;font-size: 16px;color: #000000">
                                                <p>
                                                    {}
                                                </p>
                                            </div>
                                        </div>
                                        <a style="width: 150px;height: 38px;background: #ef859d38;border-radius: 32px;display: flex;align-items: center;justify-content: center;text-decoration: none;margin: 40px auto 0" 
                                            href="{}" target="_blank">
                                            <span style="color: #DB214B">有朋自远方来</span>
                                        </a>
                                    </div>
                                    <div style="margin-top: 20px;font-size: 12px;color: #00000045">
                                        此邮件由 {} 自动发出,直接回复无效,退订请联系站长。
                                    </div>
                                </div>
                            </div>
                        </div>""";

    /**
     * 验证码发件内容 模版
     * @param verificationCode 验证码
     * @param username 用户名
     * @return
     */
    public String getCodeMailText(int verificationCode,String username) {
        String serverDomain = appConfig.getAddress().getServer();
        String mailTextStart=textStartDefault;
        if(StringUtils.hasText(username)) mailTextStart = String.format(textStart, username);
        String smallSub = String.format(imMail, "后台管理系统");
        String VerCode = String.format("【验证消息】%s为本次验证的验证码,请在5分钟内完成验证。为保证账号安全,请勿泄漏此验证码。", verificationCode);
        return generateText(backgroundUrl,
                "后台管理系统",
                smallSub,
                mailTextStart,
                VerCode,
                "https://"+serverDomain,
                serverDomain);
    }

    /**
     * 自定义生成邮件内容
     * @param backgroundUrl 背景地址
     * @param theme 主题
     * @param subtopic 副主题
     * @param contentStart 内容开头 例如:亲爱的 [收件人姓名]
     * @param content 主内容
     * @param webAddress 跳转网站地址
     * @param webName 网站名称
     * @return
     */
    public  String generateText(String backgroundUrl,String theme,String subtopic,String contentStart,String content,String webAddress,String webName){
        return StrUtil.format(mailText, backgroundUrl, theme, subtopic, contentStart, content, webAddress, webName);
    }


    public void sendMailMessage(List<String> to, String subject, String text) {
        try {
            //true代表支持复杂的类型
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mailSender.createMimeMessage(), true);
            //邮件发信人
            String nick= MimeUtility.encodeText("后台管理系统");
            mimeMessageHelper.setFrom(new InternetAddress(nick+" <"+sendMailer+">"));
            //邮件收信人1或多个
            mimeMessageHelper.setTo(to.toArray(new String[0]));
            //邮件主题
            mimeMessageHelper.setSubject(subject);
            //邮件内容
            mimeMessageHelper.setText(text, true);
            //邮件发送时间
            mimeMessageHelper.setSentDate(new Date());
            //发送邮件
            mailSender.send(mimeMessageHelper.getMimeMessage());
            logger.info("邮件发送成功,发送信息如下:");
            logger.info("收件人:{}", to);
            logger.info("主题:{}", JacksonUtil.bean2JsonStr(subject) );
            logger.info("内容:{}", JacksonUtil.bean2JsonStr(text));
        } catch (MessagingException | UnsupportedEncodingException e) {
            logger.error("邮件发送失败!",e);
        }
    }
}

3、NanoId生成(jdk17)

package cn.djshen.ms.util;

import java.security.SecureRandom;
import java.util.Random;

/**
 * @author djshen
 * @description NanoId生成工具
 * @filename NanoIdUtils
 * @Date 2023/4/27 11:20
 */
public final class NanoIdUtils {

    private NanoIdUtils() {
        //Do Nothing
    }

    public static final SecureRandom DEFAULT_NUMBER_GENERATOR = new SecureRandom();

    public static final char[] DEFAULT_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    public static final int DEFAULT_SIZE = 21;

    public static String randomNanoId() {
        return randomNanoId(DEFAULT_NUMBER_GENERATOR, DEFAULT_ALPHABET, DEFAULT_SIZE);
    }

    private static String randomNanoId(final Random random, final char[] alphabet, final int size) {

        if (random == null)
            throw new IllegalArgumentException("random cannot be null.");

        if (alphabet == null)
            throw new IllegalArgumentException("alphabet cannot be null.");

        if (alphabet.length == 0 || alphabet.length >= 256)
            throw new IllegalArgumentException("alphabet must contain between 1 and 255 symbols.");

        if (size <= 0)
            throw new IllegalArgumentException("size must be greater than zero.");

        final int mask = (2 << (int) Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1;

        final int step = (int) Math.ceil(1.6 * mask * size / alphabet.length);

        final StringBuilder idBuilder = new StringBuilder();

        while (true) {
            final byte[] bytes = new byte[step];
            random.nextBytes(bytes);
            for (int i = 0; i < step; i++) {
                final int alphabetIndex = bytes[i] & mask;
                if (alphabetIndex < alphabet.length) {
                    idBuilder.append(alphabet[alphabetIndex]);
                    if (idBuilder.length() == size) {
                        return idBuilder.toString();
                    }
                }
            }
        }
    }

}

4、Jackson工具类(jdk17)

package cn.djshen.ms.util;

import cn.djshen.ms.common.CustomException;
import cn.djshen.ms.common.ResultStatus;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * @author djshen
 * @description jackson工具类
 * @filename JacksonUtil
 * @Date 2023/7/8 22:21
 */
public class JacksonUtil {
    private static final Logger logger= LoggerFactory.getLogger(JacksonUtil.class);

    /**
     * 为了保证多线程安全,用ThreadLocal,让每个线程都能new一个新的ObjectMapper防止多线程安全问题。
     */
    private static final ThreadLocal<ObjectMapper> objectMapperThreadLocal = ThreadLocal.withInitial(()->{
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);//忽略未知的JSON字段,默认开启,会抛出异常
        objectMapper.registerModule(new JavaTimeModule()); // 启用对Java 8日期/时间类型的处理 例如LocalDateTime
        /*objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);//如果基本类型为null则抛出异常,默认关闭*/
        return objectMapper;
    });
    /**
     * 创建一个ObjectNode对象 相当于fastjson的JsonObject
     * @return
     */
    public static ObjectNode createObjectNode(){
        return new ObjectMapper().createObjectNode();
    }

    /**
     * 创建一个ArrayNode对象 相当于fastjson的JsonArray
     * @return
     */
    public static ArrayNode createArrayNode(){
        return new ObjectMapper().createArrayNode();
    }

    /**
     * 转换javabean类型
     * @param obj
     * @return
     * @param <O,C>
     */
    public static <O,C> C bean2Class(O obj,Class<C> convertClass)  {
        return jsonStr2Bean(bean2JsonStr(obj), convertClass);
    }

    /**
     * 将javabean 转为 json字符串
     * @param obj
     * @return
     * @param <T>
     */
    public static <T> String bean2JsonStr(T obj)  {
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            StringWriter sw = new StringWriter();
            JsonGenerator gen = objectMapper.getFactory().createGenerator(sw);
            objectMapper.writeValue(gen, obj);
            gen.close();
            return sw.toString();
        }catch (IOException e){
            logger.error("jackson bean2JsonStr error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson bean2JsonStr error");
        }
    }

    /**
     * 将json字符串 转为 javabean
     * @param jsonStr
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T jsonStr2Bean(String jsonStr, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(jsonStr, objClass);
        } catch (JsonProcessingException e) {
            logger.error("jackson JsonStr2Bean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson JsonStr2Bean error");
        }
    }

    /**
     * 将字符输入流 转为 javabean
     * @param reader
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T stringReader2JavaBean(Reader reader, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(reader, objClass);
        } catch (IOException e) {
            logger.error("jackson stringReader2JavaBean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson stringReader2JavaBean error");
        }
    }

    /**
     * 将file 转为 javabean
     * @param file
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T file2JavaBean(File file, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(file, objClass);
        } catch (IOException e) {
            logger.error("jackson file2JavaBean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson file2JavaBean error");
        }
    }

    /**
     * 将URL 转为 javabean
     * @param url
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T URL2JavaBean(URL url, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(url, objClass);
        } catch (IOException e) {
            logger.error("jackson URL2JavaBean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson URL2JavaBean error");
        }
    }

    /**
     * 将InputStream 转为 javabean
     * @param inputStream
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T inputStream2JavaBean(InputStream inputStream, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(inputStream, objClass);
        } catch (IOException e) {
            logger.error("jackson inputStream2JavaBean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson inputStream2JavaBean error");
        }
    }

    /**
     * 将byte[] 转为 javabean
     * @param bytes
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> T bytes2JavaBean(byte[] bytes, Class<T> objClass){
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(bytes, objClass);
        } catch (IOException e) {
            logger.error("jackson bytes2JavaBean error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson bytes2JavaBean error");
        }
    }

    /**
     * 将json数组字符串 转为 java对象
     * @param jsonArrayStr
     * @param objClass
     * @return
     * @param <T>
     */
    public static <T> List<T> jsonArrayStr2List(String jsonArrayStr, Class<T> objClass) {
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            JavaType listType = objectMapper.getTypeFactory().constructCollectionType(List.class, objClass);
            return objectMapper.readValue(jsonArrayStr, listType);
        } catch (IOException e) {
            logger.error("jackson jsonArrayStr2List error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson jsonArrayStr2List error");
        }
    }

    /**
     * 将json字符串 转为 map
     * @param jsonObjectStr
     * @return
     */
    public static  Map<String, Object> jsonObjectStr2Map(String jsonObjectStr) {
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            return objectMapper.readValue(jsonObjectStr, new TypeReference<>() {});
        } catch (IOException e) {
            logger.error("jackson jsonObjectStr2Map error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson jsonObjectStr2Map error");
        }
    }

    /**
     * 将json字符串 转为 map 可指定k,v的类型
     * @param jsonObjectStr
     * @param keyClass
     * @param valueClass
     * @return
     * @param <K>
     * @param <V>
     */
    public static <K, V> Map<K, V> jsonObjectStr2MapCustomClass(String jsonObjectStr, Class<K> keyClass, Class<V> valueClass) {
        try {
            ObjectMapper objectMapper = objectMapperThreadLocal.get();
            JavaType mapType = objectMapper.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);
            return objectMapper.readValue(jsonObjectStr, mapType);
        } catch (IOException e) {
            logger.error("jackson jsonObjectStr2MapCustomClass error",e);
            throw new CustomException(ResultStatus.ERROR,"jackson jsonObjectStr2MapCustomClass error");
        }
    }

}

5、SpringUtil

package cn.djshen.ms.util;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;


@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
    /** Spring应用上下文环境 */
    private static ConfigurableListableBeanFactory beanFactory;

    private static ApplicationContext applicationContext;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        SpringUtils.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        SpringUtils.applicationContext = applicationContext;
    }

    /**
     * 获取对象
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException
    {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     * @param clz
     * @return
     * @throws BeansException
     */
    public static <T> T getBean(Class<T> clz) throws BeansException
    {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name)
    {
        return beanFactory.containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     * @param name
     * @return boolean
     * @throws NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws NoSuchBeanDefinitionException
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     * @param name
     * @return
     * @throws NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException
    {
        return beanFactory.getAliases(name);
    }

    /**
     * 获取aop代理对象
     * @param invoker
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAopProxy(T invoker)
    {
        return (T) AopContext.currentProxy();
    }

    /**
     * 获取当前的环境配置,无配置返回null
     * @return 当前的环境配置(字符串数组)
     */
    public static String[] getActiveProfiles()
    {
        return applicationContext.getEnvironment().getActiveProfiles();
    }

    /**
     * 获取当前的环境配置,当有多个环境配置时,只获取第一个
     * @return 当前的环境配置
     */
    public static String getActiveProfile()
    {
        final String[] activeProfiles = getActiveProfiles();
        return activeProfiles.length>0 ? activeProfiles[0] : null;
    }
}

6、redis序列化工具类

6.1、Redis使用FastJson序列化

package cn.djshen.ms.config.redis;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Redis使用FastJson序列化
 *
 * @author sg
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private final Class<T> clazz;

    static
    {
        // version@1.2.72 升级到 2 需要注释
        /*ParserConfig.getGlobalInstance().setAutoTypeSupport(true);*/
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

6.2、Redis使用Jackson序列化

package cn.djshen.ms.config.redis;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Redis使用Jackson序列化
 *
 * @param <T> the type of objects to serialize
 */
public class JacksonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private final Class<T> clazz;
    private final ObjectMapper objectMapper;

    public JacksonRedisSerializer(Class<T> clazz) {
        this.clazz = clazz;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        try {
            return objectMapper.writeValueAsBytes(t);
        } catch (Exception e) {
            throw new SerializationException("Error serializing object to JSON: " + t, e);
        }
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        try {
            return objectMapper.readValue(bytes, clazz);
        } catch (Exception e) {
            throw new SerializationException("Error deserializing object from JSON: " + new String(bytes, DEFAULT_CHARSET), e);
        }
    }
}

6.3、redis配置工程类

package cn.djshen.ms.config.redis;

import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisConfig {

    @Bean
    @Primary
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        /*FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);*/
        JacksonRedisSerializer serializer=new JacksonRedisSerializer(Object.class);

        //默认所有的都优先采用该方式进行序列化
        template.setDefaultSerializer(serializer);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        /*FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);*/
        JacksonRedisSerializer<Object> serializer= new JacksonRedisSerializer<>(Object.class);
        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config =
                RedisCacheConfiguration.defaultCacheConfig()
                        /*.entryTtl(Duration.ofSeconds(86400))*/  //过期时间86400秒
                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                        .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

6.4、redis工具类

package cn.djshen.ms.util;

import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author djshen
 * @description redis工具类
 * @filename NanoIdUtils
 * @Date 2023/4/27 11:20
 */
@Component
public class RedisUtil {
    @Resource
    private RedisTemplate<Object, Object> redisTemplate;


    /**  默认过期时长,单位:秒 */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
    /**  不设置过期时长 */
    public final static long NOT_EXPIRE = -1;

    /**
     * 在redis中设置key-value缓存
     * @param key 上传的key
     * @param value 上传key对应的值
     * @param expire 过期时间(秒) -1不设置
     */
    public <T> void set(String key, T value, long expire){
        redisTemplate.opsForValue().set(key, value);
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
    }

    /**
     * 在redis中设置key-value缓存(默认时间为一天)
     * @param key 上传的key
     * @param value 上传key对应的值
     */
    public <T> void set(String key, T value){
        set(key, value, DEFAULT_EXPIRE);
    }

    /**
     * 根据key获取value,并根据对象类型进行转换,且重置过期时间
     * @param key 想获取value的key
     * @param clazz value想转换的类型
     * @param expire 过期时间(秒) -1不设置
     * @return value
     * @param <T> 返回需要的对象类型
     */
    public <T> T get(String key, Class<T> clazz, long expire) {
        Object value =  redisTemplate.opsForValue().get(key);
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return ObjectUtils.isEmpty(value) ? null : JacksonUtil.bean2Class(value,clazz);
    }

    /**
     * 根据key获取value,并根据对象类型进行转换,不重置过期时间
     * @param key 想获取value的key
     * @param clazz value想转换的类型
     * @return value
     * @param <T> 返回需要的对象类型
     */
    public <T> T get(String key, Class<T> clazz) {
        return get(key, clazz, NOT_EXPIRE);
    }

    /**
     * 根据key获取value,且重置过期时间
     * @param key 想获取value的key
     * @param expire 过期时间(秒) -1不设置
     * @return value的字符串
     */
    public String get(String key, long expire) {
        String value = (String) redisTemplate.opsForValue().get(key);
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return value;
    }

    /**
     * 根据key获取value,不重置过期时间
     * @param key 想获取value的key
     * @return value的字符串
     */
    public String get(String key) {
        return get(key, NOT_EXPIRE);
    }

    /**
     * 得到key存储的时间
     * @param key 你需要得到的key
     * @return 时间大小,单位:秒
     */
    public Long getExpire(String key) {
        return  redisTemplate.getExpire(key);
    }

    /**
     * 删除key
     * @param key 你需要删除的key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }


}

7、加密工具

7.1、字节数组加密工具

package cn.djshen.ms.util;

import cn.djshen.ms.common.CustomException;
import cn.djshen.ms.common.ResultStatus;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.Arrays;

/**
 * @author djshen
 * @description 字节数组加密工具
 * @filename ByteArrayEncryptionUtil
 * @Date 2023/7/8 22:21
 */
public class ByteArrayEncryptionUtil {
    private static final String DEFAULT_KEY = "funsyabcdefgcdfg";
    private static final String DEFAULT_ALGORITHM = "AES/CBC/PKCS5Padding";
    private static final int DEFAULT_IV_SIZE = 16;//指定DEFAULT_KEY字符数量
    private static final String DEFAULT_CHARSET = "UTF-8";

    public static void main(String[] args) throws Exception {
        String filepath="E:\\download\\Edge\\T-7Fnmp-TyUIFvQf5Y6hB.webp";
        //NIO 将文件转换为字节数组
        Path path = Paths.get(filepath);
        byte[] bFile = Files.readAllBytes(path);
        //加密
        /*byte[] encrypt = ByteArrayEncryptionUtil.encrypt(bFile);*/
        /*Files.write(path, encrypt);*/
        //解密
        byte[] decrypt = ByteArrayEncryptionUtil.decrypt(bFile);
        Files.write(path, decrypt);
    }
    
    /**
     * 使用指定的密钥和算法加密给定的字节数组。
     * @param bytes 要加密的字节数组
     * @param key 用于加密的密钥
     * @param algorithm 要使用的加密算法
     * @return 加密字节数组
     * @throws Exception 如果加密失败
     */
    public static byte[] encrypt(byte[] bytes, String key, String algorithm)  {
        try {
            Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, algorithm);
            byte[] encryptedBytes = cipher.doFinal(bytes);
            byte[] iv = cipher.getIV();
            byte[] result = new byte[iv.length + encryptedBytes.length];
            System.arraycopy(iv, 0, result, 0, iv.length);
            System.arraycopy(encryptedBytes, 0, result, iv.length, encryptedBytes.length);
            return result;
        }catch (Exception e){
            throw new CustomException(ResultStatus.ERROR,"字节数组加密失败");
        }
    }
    
    /**
     * 使用指定的密钥和算法解密给定的字节数组
     * @param bytes 要解密的字节数组
     * @param key 用于解密的密钥
     * @param algorithm 要使用的加密算法
     * @return 解密的字节数组
     * @throws Exception 如果解密失败
     */
    public static byte[] decrypt(byte[] bytes, String key, String algorithm)  {
        try {
            byte[] iv = Arrays.copyOfRange(bytes, 0, DEFAULT_IV_SIZE);
            byte[] encryptedBytes = Arrays.copyOfRange(bytes, DEFAULT_IV_SIZE, bytes.length);
            Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, algorithm, iv);
            return cipher.doFinal(encryptedBytes);
        }catch (Exception e){
            throw new CustomException(ResultStatus.ERROR,"字节数组解密失败");
        }
    }
    
    /**
     * 使用默认密钥和算法加密给定的字节数组。
     * @param bytes 要加密的字节数组
     * @return 加密字节数组
     * @throws Exception 如果加密失败
     */
    public static byte[] encrypt(byte[] bytes)  {
        return encrypt(bytes, DEFAULT_KEY, DEFAULT_ALGORITHM);
    }
    
    /**
     * 使用默认密钥和算法解密给定的字节数组。
     * @param bytes 要解密的字节数组
     * @return 要解密的字节数组
     * @throws Exception 如果解密失败
     */
    public static byte[] decrypt(byte[] bytes) {
        return decrypt(bytes, DEFAULT_KEY, DEFAULT_ALGORITHM);
    }
    
    private static Cipher getCipher(int mode, String key, String algorithm) throws Exception {
        byte[] keyBytes = key.getBytes(DEFAULT_CHARSET);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, algorithm.split("/")[0]);
        IvParameterSpec ivSpec = generateIV(DEFAULT_IV_SIZE);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(mode, keySpec, ivSpec);
        return cipher;
    }
    
    private static Cipher getCipher(int mode, String key, String algorithm, byte[] iv) throws Exception {
        byte[] keyBytes = key.getBytes(DEFAULT_CHARSET);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, algorithm.split("/")[0]);
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(mode, keySpec, ivSpec);
        return cipher;
    }
    
    private static IvParameterSpec generateIV(int size) {
        byte[] iv = new byte[size];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }
    
    /**
     * 将给定字符串添加到给定字节数组的开头。
     * @param bytes 将向其添加字符串的字节数组
     * @param s 要添加的字符串
     * @return 修改后的字节数组
     * @throws Exception 如果加密失败
     */
    public static byte[] addString(byte[] bytes, String s) throws Exception {
        byte[] stringBytes = s.getBytes(DEFAULT_CHARSET);
        byte[] result = new byte[stringBytes.length + bytes.length];
        System.arraycopy(stringBytes, 0, result, 0, stringBytes.length);
        System.arraycopy(bytes, 0, result, stringBytes.length, bytes.length);
        return result;
    }
    
    /**
     * 从给定字节数组的开头移除给定长度的字符串。
     * @param bytes 要从中删除字符串的字节数组
     * @param length 要删除的字符串的长度
     * @return 修改后的字节数组
     * @throws Exception 如果解密失败
     */
    public static byte[] removeString(byte[] bytes, int length) throws Exception {
        byte[] stringBytes = Arrays.copyOfRange(bytes, 0, length);
        String s = new String(stringBytes, DEFAULT_CHARSET);
        byte[] result = Arrays.copyOfRange(bytes, length, bytes.length);
        return result;
    }
}

8、JWT工具类

package cn.djshen.ms.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;

import java.util.Date;

/**
 * @author djshen
 * @description jwt工具类
 * @filename JwtUtils
 * @Date 2023/4/27 11:20
 */
public class JwtUtils {
    public static final long EXPIRE = 1000 * 60 * 60 * 24;//token过期时间(一天)
    public static final String APP_SECRET = "admin.funsy.cn";//自己设定的规则
    //生成token字符串方法
    public static String getJwtToken(String username, String password){
        return Jwts.builder()
                //设置jwt头
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //设置过期时间
                .setSubject("slyfun-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                //设置token主体部分,用户信息
                .claim("username", username)
                .claim("password", password)
                //根据规则加密
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }
    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(!StringUtils.hasText(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据请求头中的token获取会员id
     * @param request
     * @return
     */
    public static String getPasswordByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(!StringUtils.hasText(jwtToken)) return "";
        //根据规则解析token获得用户password
        Jws<Claims> claimsJws =
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("password");
    }

    /**
     * 根据请求头中的token获取会员name
     * @param request
     * @return
     */
    public static String getNameByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(!StringUtils.hasText(jwtToken)) return "";
        //根据规则解析token获得用户name
        Jws<Claims> claimsJws =
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("username");
    }
    /**
     * 根据token获取id
     * @param token
     * @return
     */
    public static String getPasswordByToken(String token){
        if(!StringUtils.hasText(token)) return "";
        //根据规则解析token获得用户password
        Jws<Claims> claimsJws =
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("password");
    }

    /**
     * 根据token获取会员名
     * @param token
     * @return
     */
    public static String getNameByToken(String token){
        if(!StringUtils.hasText(token)) return "";
        //根据规则解析token获得用户id
        Jws<Claims> claimsJws =
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("username");
    }
}

9、sqlite工具类

9.1、sqlite读写锁

package cn.djshen.ms.config.sqlite;

import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * sqlite哪些方法需要添加写锁
 */
public enum SqlLiteDaoMethod {
    UPDATE("sqlite 更新操作", "update"),
    DELETE("sqlite 删除操作", "delete"),
    SAVE("sqlite 保存操作", "save"),
    INSERT("sqlite 插入操作", "insert");
    private String description;
    private String name;

    SqlLiteDaoMethod() {
    }

    SqlLiteDaoMethod(String description, String name) {
        this.description = description;
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static final List<String> daoMethodNameList = new ArrayList<>();

    public static List<String> getMethodNameList() {
        if (CollectionUtils.isEmpty(daoMethodNameList)) {
            SqlLiteDaoMethod[] values = SqlLiteDaoMethod.values();
            for (SqlLiteDaoMethod value : values) {
                daoMethodNameList.add(value.name);
            }
        }
        return daoMethodNameList;
    }
}
package cn.djshen.ms.config.sqlite;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Aspect
@Component
public class SqlLiteSyncAspect {
    private static final Logger logger= LoggerFactory.getLogger(SqlLiteSyncAspect.class);
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    @Pointcut("execution(public * cn.djshen.ms.dao.ms.*.*(..)))")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        String name = pjp.getSignature().getName();
        logger.debug("方法环绕proceed,class - {}, 方法是 - {}", pjp.getTarget().getClass().getName(), name);
        if (SqlLiteDaoMethod.getMethodNameList().stream().anyMatch(name::contains)) {
            readWriteLock.writeLock().lock();
            try {
                Object proceed = pjp.proceed();
                return proceed;
            } catch (Exception e) {
                logger.error("sqlite数据库操作时,写锁保护代码发生异常", e);
                throw e;
            } finally {
                readWriteLock.writeLock().unlock();
            }
        } else {
            try {
                readWriteLock.readLock().lock();
                return pjp.proceed();
            } catch (Exception e) {
                logger.error("sqlite数据库操作时,读锁保护代码发生异常", e);
                throw e;
            } finally {
                readWriteLock.readLock().unlock();
            }
        }
    }
}

10、swagger配置类

 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' //swagger 3.0 api文档引入

#swagger配置文件的配置
springdoc:
  swagger-ui:
    enabled: false #是否开启swagger页面
  api-docs:
    enabled: false #是否开启API文档
package cn.djshen.ms.config.swagger;

import cn.djshen.ms.config.AppConfig;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import jakarta.annotation.Resource;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author djshen
 * @description springdoc-openapi配置
 * @filename SwaggerConfig
 * @Date 2023/4/27 22:01
 */
@Configuration
public class SwaggerConfig {
    @Resource
    private AppConfig appConfig;
    /**文档信息配置*/
    @Bean
    public OpenAPI OpenAPI() {
        return new OpenAPI()
                .info(new Info().title("管理系统API接口文档")
                        .description("管理系统应用")
                        .version("v0.0.1")
                        .license(new License().name("Apache 2.0").url("https://springdoc.org")))
                .externalDocs(new ExternalDocumentation()
                        .description("MS线上地址")
                        .url("https://"+appConfig.getAddress().getServer())
                        .description("管理系统(Management Service) Github地址")
                        .url("https://github.com/shendongjun/ms-service"));
    }

    /**根据需求进行接口分组*/
    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
                .group("MS-全部接口")
                .pathsToMatch("/**")
                .build();
    }
    @Bean
    public GroupedOpenApi testApi() {
        return GroupedOpenApi.builder()
                .group("MS-测试接口")
                .pathsToMatch("/test/**")
                .build();
    }


}

11、aop

11.1、接口防刷

package cn.djshen.ms.config.aop.throttling;


import java.lang.annotation.*;


/**
 * @author djshen
 * @description 接口防刷注解  使用:在相应需要防刷的方法上加上该注解,即可
 * @filename Prevent
 * @Date 2023/4/27 11:20
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Throttling {


    /**
     * 限制的时间值(秒)
     *
     * @return
     */
    String value() default "60";


    /**
     * 提示
     */
    String message() default "";

    /**
     * 策略
     * @return
     */
    ThrottlingStrategy strategy() default ThrottlingStrategy.DEFAULT;
}
package cn.djshen.ms.config.aop.throttling;


import cn.djshen.ms.common.CustomException;
import cn.djshen.ms.common.ResultStatus;
import cn.djshen.ms.util.RedisUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;


/**
 * @author djshen
 * @description 防刷切面实现类,本切面只能根据方法进行防刷,不能根据重构方法进行防刷
 * @filename PreventAop
 * @Date 2023/4/27 11:20
 */
@Aspect
@Component
public class ThrottlingAop {
    private static final Logger logger = LoggerFactory.getLogger(ThrottlingAop.class);

    @Resource
    private RedisUtil redisUtil;
    /**
     * 切入点
     */
    @Pointcut("@annotation(cn.djshen.ms.config.aop.throttling.Throttling)")
    public void pointcut() {
    }

    /**
     * 处理前
     *
     * @return
     */
    @Before("pointcut()")
    public void joinPoint(JoinPoint joinPoint) throws Exception {
        /*for (Object arg : joinPoint.getArgs()) {

        }*/
        //获取方法位置
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = joinPoint.getTarget().getClass().getMethod(methodSignature.getName(),
                methodSignature.getParameterTypes());

        Throttling preventAnnotation = method.getAnnotation(Throttling.class);
        String methodFullName = method.getDeclaringClass().getName() + method.getName();

        entrance(preventAnnotation,methodFullName);
    }


    /**
     * 入口
     *
     * @param prevent
     */
    private void entrance(Throttling prevent, String methodFullName) throws Exception {
        ThrottlingStrategy strategy = prevent.strategy();
        switch (strategy) {
            case DEFAULT -> defaultHandle(prevent, methodFullName);
            default -> throw new CustomException(ResultStatus.ERROR.getCode(), false, "无效的策略");
        }
    }

    /**
     * 默认处理方式
     *
     * @param prevent
     */
    private void defaultHandle(Throttling prevent, String methodFullName) {
        logger.info("开启防刷: 方法 {} ,时间 {} 秒 ",methodFullName,prevent.value());
        String key = "prevent:"+methodFullName;
        String resp = redisUtil.get(key);
        if (StringUtils.isEmpty(resp)) { // 如果存在,则禁止调用
            long expire = Long.parseLong(prevent.value());
            redisUtil.set(key, methodFullName, expire);
        } else {
            String message = !StringUtils.isEmpty(prevent.message()) ? prevent.message() :
                    redisUtil.getExpire(key) + "秒后在尝试,请勿在此期间重复尝试";
            throw new CustomException(ResultStatus.ERROR.getCode(),false,message);
        }
    }
}
package cn.djshen.ms.config.aop.throttling;

/**
 * @author djshen
 * @description 防刷策略枚举
 * @filename PreventStrategy
 * @Date 2023/4/27 11:20
 */
public enum ThrottlingStrategy {
    /**
     * 默认策略
     */
    DEFAULT
}

12、mybatis 支持FASTJSON

import com.alibaba.fastjson.JSONArray;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
 
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
/**
 * Created by lixio on 2019/3/28 20:51
 * @description 用以mysql中json格式的字段,进行转换的自定义转换器,转换为实体类的JSONArray属性
 * MappedTypes注解中的类代表此转换器可以自动转换为的java对象
 * MappedJdbcTypes注解中设置的是对应的jdbctype
 */
@MappedTypes(JSONArray.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ArrayJsonHandler extends BaseTypeHandler<JSONArray> {
    //设置非空参数
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, JSONArray parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, String.valueOf(parameter.toJSONString()));
    }
    //根据列名,获取可以为空的结果
    @Override
    public JSONArray getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String sqlJson = rs.getString(columnName);
        if (null != sqlJson){
            return JSONArray.parseArray(sqlJson);
        }
        return null;
    }
    //根据列索引,获取可以为空的结果
    @Override
    public JSONArray getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String sqlJson = rs.getString(columnIndex);
        if (null != sqlJson){
            return JSONArray.parseArray(sqlJson);
        }
        return null;
    }
 
    @Override
    public JSONArray getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String sqlJson = cs.getString(columnIndex);
        if (null != sqlJson){
            return JSONArray.parseArray(sqlJson);
        }
        return null;
    }
 
}
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
 
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
/**
 * Created by lixio on 2019/3/28 15:44
 * @description 用以mysql中json格式的字段,进行转换的自定义转换器,转换为实体类的JSONObject属性
 * MappedTypes注解中的类代表此转换器可以自动转换为的java对象
 * MappedJdbcTypes注解中设置的是对应的jdbctype
 */
 
@MappedTypes(JSONObject.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ObjectJsonHandler extends BaseTypeHandler<JSONObject>{
 
    //设置非空参数
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, JSONObject parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, String.valueOf(parameter.toJSONString()));
    }
    //根据列名,获取可以为空的结果
    @Override
    public JSONObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String sqlJson = rs.getString(columnName);
        if (null != sqlJson){
            return JSONObject.parseObject(sqlJson);
        }
        return null;
    }
    //根据列索引,获取可以为空的结果
    @Override
    public JSONObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String sqlJson = rs.getString(columnIndex);
        if (null != sqlJson){
            return JSONObject.parseObject(sqlJson);
        }
        return null;
    }
 
    @Override
    public JSONObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String sqlJson = cs.getString(columnIndex);
        if (null != sqlJson){
            return JSONObject.parseObject(sqlJson);
        }
        return null;
    }
}

13、RTSP网络判断工具类 (jdk8)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.SocketUtils;
import org.springframework.util.StringUtils;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author shendongjun
 * @create 2023-10-12 9:16
 * @describe 关于网络的工具类
 */
public class NetUtil {
    private static final Logger logger = LoggerFactory.getLogger(NetUtil.class);

    public static void main(String[] args) {
        /**
         * 测试 checkRtspConnection方法
         */
        //海康测试
        /*int i = checkRtspConnection("rtsp://192.168.1.82:554/h264/ch33/main/av_stream",
                "192.168.1.82", 554, "admin", "xtkj12345",
                SocketUtils.findAvailableUdpPort(40000, 50000),
                SocketUtils.findAvailableUdpPort(50000, 60000),100);*/
        //大华测试
        int i = checkRtspConnection("rtsp://192.168.1.84:554/cam/realmonitor?channel=1&subtype=0",
                "192.168.1.84", 554, "admin", "xtkj12345",
                SocketUtils.findAvailableUdpPort(50000, 60000),
                SocketUtils.findAvailableUdpPort(50000, 60000), 100);
        System.out.println(i);
    }

    /**
     * 判断ip下端口是否可连接
     * @param ipAddress ip地址
     * @param port 端口
     * @param timeout 连接超时时间 ms
     * @return 是否端口开放的
     */
    public static boolean isPortOpen(String ipAddress, int port, int timeout) {
        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(ipAddress, port), timeout);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 检查rtsp地址是否存在问题
     * @param rtspUrl rtsp地址
     * @param host 相机ip地址
     * @param port 相机端口
     * @param username 相机用户名
     * @param password 相机密码
     * @param RTPPort RTP协议端口 需要使用本机空闲UDP端口 用于监听RTSP有无数据传输到本机
     * @param AVPPort AVP协议端口 同上
     * @return 返回rtsp的状态  (1 正常 2 账户密码错误 3 RTSP服务端发生的流量检测不到 4 ip端口连接超时 5 其他错误)
     */
    public static int checkRtspConnection(String rtspUrl,String host,int port,String username,String password,int RTPPort,int AVPPort,int timeout) {
        //注意:海康path路径错了或默认主码流路径,大华则会报错404,455
        if (!isPortOpen(host,port,timeout)) return 4;
        try (Socket socket = new Socket(host, port)){
            //第一次提交 发送 OPTIONS 请求
            int count=2;
            String optionsRequest = "OPTIONS " + rtspUrl + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n\r\n";
            // 读取服务器返回的响应
            String response = socketAuthor(socket, optionsRequest);
            logger.debug("服务器第一次请求:{}", optionsRequest);
            logger.debug("服务器第一次响应:{}", response);
            //第二次提交 发送 DESCRIBE 请求
            String optionsRequest2 = "DESCRIBE " + rtspUrl + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                    "Accept: application/sdp\r\n\r\n";
            response = socketAuthor(socket,optionsRequest2);
            logger.debug("服务器第二次请求:{}" , optionsRequest2);
            logger.debug("服务器第二次响应:{}" , response);
            String realm = extractValue(response, "realm=\"([^\"]+)\"");
            String nonce = extractValue(response, "nonce=\"([^\"]+)\"");
            logger.debug("Realm: {}" , realm);
            logger.debug("Nonce: {}" , nonce);
            // 第三次提交 发送 DESCRIBE 请求
            //digest认证方式一
            String describeMd5 = calculateDigestResponse(username, realm, password, "DESCRIBE", rtspUrl, nonce);
            //digest认证方式二
            /*String md5=calculateMD5(password+":"+nonce+":"+calculateMD5("DESCRIBE:"+rtspUrl));*/
            String authenticateHeader = "Digest username=\"" + username + "\", "
                    + "realm=\"" + realm + "\", "
                    + "nonce=\"" + nonce + "\", "
                    + "uri=\"" + rtspUrl + "\", "
                    + "response=\"" + describeMd5 + "\"";
            String optionsRequest3 = "DESCRIBE " + rtspUrl + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "Authorization: " +authenticateHeader + "\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                    "Accept: application/sdp\r\n\r\n";
            response = socketAuthor(socket, optionsRequest3);
            logger.debug("服务器第三次请求:{}" , optionsRequest3);
            logger.debug("服务器第三次响应:{}" , response);
            if(response.contains("401 Unauthorized")){
                return 2;
            }
            //第四次提交 发送 SETUP 请求
            //获取 trackID  Content-Base后面的路径
            String trackID = extractValue(response, "trackID=(\\d+)");
            String contentBase = extractValue(response, "Content-Base: (.*)");
            logger.debug("trackID : {}",trackID);
            logger.debug("contentBase :{}",contentBase);
            String setupMd5 = calculateDigestResponse(username, realm, password, "SETUP", contentBase, nonce);
            String setupAuthenticate = "Digest username=\"" + username + "\", "
                    + "realm=\"" + realm + "\", "
                    + "nonce=\"" + nonce + "\", "
                    + "uri=\"" + contentBase + "\", "
                    + "response=\"" + setupMd5 + "\"";
            String optionsRequest4 = "SETUP " + contentBase+"trackID="+trackID + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "Authorization: " +setupAuthenticate + "\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                    "Transport: RTP/AVP/UDP;unicast;client_port="+RTPPort+","+AVPPort+"\r\n\r\n";
            //上面的client_port端口记得本地不要被别的应用占用,不然也会传输失败
            response = socketAuthor(socket, optionsRequest4);
            logger.debug("服务器第四次请求:{}" , optionsRequest4);
            logger.debug("服务器第四次响应:{}" , response);
            //第五次提交 发送 PLAY 请求
            //获取Session
            String Session= extractValue(response,"Session: (.*);").trim();
            String playMd5 = calculateDigestResponse(username, realm, password, "PLAY", contentBase, nonce);
            String playAuthenticate = "Digest username=\"" + username + "\", "
                    + "realm=\"" + realm + "\", "
                    + "nonce=\"" + nonce + "\", "
                    + "uri=\"" + contentBase + "\", "
                    + "response=\"" + playMd5 + "\"";
            String optionsRequest5 = "PLAY " + contentBase + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "Authorization: " +playAuthenticate + "\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                    "Session: "+Session+"\r\n" +
                    "Range: npt=0.000-\r\n\r\n";
            //上面的client_port端口记得本地不要被别的应用占用,不然也会传输失败
            response = socketAuthor(socket, optionsRequest5);
            logger.debug("服务器第五次请求:{}" , optionsRequest5);
            logger.debug("服务器第五次响应:{}" , response);
            //第六次提交 发送 GET_PARAMETER 请求 (循环若干次)
            //开启线程监听,判断是否能接受到视频流数据,在main方法中不要开启,可以写一个controller测试
            //只需要监听RTP,因为RTP协议才是主要传输数据的,AVP协议跟随一些配置设置 偶数RTP 奇数AVP
            boolean hasData = listenerUdp(RTPPort,timeout);
            //这个报文不需要在研究,因为这个报文请求只是用于持续连接的
            /*while (count<8){
                String getMd6 = calculateDigestResponse(username, realm, password, "GET_PARAMETER", contentBase, nonce);
                String getAuthenticate = "Digest username=\"" + username + "\", "
                        + "realm=\"" + realm + "\", "
                        + "nonce=\"" + nonce + "\", "
                        + "uri=\"" + contentBase + "\", "
                        + "response=\"" + getMd6 + "\"";
                String optionsRequest6 = "GET_PARAMETER " + contentBase + " RTSP/1.0\r\n" +
                        "CSeq: "+count+++"\r\n" +
                        "Authorization: " +getAuthenticate + "\r\n" +
                        "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                        "Session: "+Session+"\r\n" +
                        "\r\n";
                response = socketAuthor(socket, optionsRequest6);
                logger.debug("服务器第"+(count-1)+"次请求:{}" , optionsRequest6);
                logger.debug("服务器第"+(count-1)+"次响应:{}}" , response);
            }*/

            //最后一次提交 发送 TEARDOWN  让 服务器 不在往指定RTP,AVP端口发送数据了
            String downMd5 = calculateDigestResponse(username, realm, password, "TEARDOWN", contentBase, nonce);
            String downAuthenticate = "Digest username=\"" + username + "\", "
                    + "realm=\"" + realm + "\", "
                    + "nonce=\"" + nonce + "\", "
                    + "uri=\"" + contentBase + "\", "
                    + "response=\"" + downMd5 + "\"";
            String optionsRequestE = "TEARDOWN " + contentBase + " RTSP/1.0\r\n" +
                    "CSeq: "+count+++"\r\n" +
                    "Authorization: " +downAuthenticate + "\r\n" +
                    "User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n" +
                    "Session: "+Session+"\r\n" +
                    "\r\n";
            //上面的client_port端口记得本地不要被别的应用占用,不然也会传输失败
            response = socketAuthor(socket, optionsRequestE);
            logger.debug("服务器第"+(count-1)+"次请求:{}" , optionsRequestE);
            logger.debug("服务器第"+(count-1)+"次响应:{}" , response);
            if(hasData){
                return 1;
            }else {
                return 3;
            }
        } catch (Exception e) {
            logger.error("RTSP 协议不可用",e);
        }
        return 5;
    }

    public static boolean listenerUdp(int RTPPort,int timeout){
        try (DatagramSocket socketRTP = new DatagramSocket(RTPPort)){
            byte[] buffer = new byte[1024];
            DatagramPacket packetRTP = new DatagramPacket(buffer, buffer.length);
            boolean hasData=false;
            socketRTP.setSoTimeout(timeout);
            for (int i = 0; i < 3; i++) {
                try {
                    socketRTP.receive(packetRTP); // RTP接收UDP数据包
                } catch (SocketTimeoutException e) {
                    // 接受数据设置超时时间
                    break;
                }
                // 处理接收到的数据
                String dataRTP = new String(packetRTP.getData(), 0, packetRTP.getLength());
                if(StringUtils.hasText(dataRTP) ){
                    hasData=true;
                    break;
                }
                // 清空缓冲区
                packetRTP.setLength(buffer.length);
            }
            return hasData;
        } catch (Exception e) {
            logger.error("监听RTP,AVP端口出错",e);
        }
        return false;
    }

    /**
     * digest认证方式二
     * response = md5(md5(username:realm:password):nonce:md5(public_method:url))
     * @param username 设备用户名
     * @param realm DESCRIBE请求返回到realm值
     * @param password 设备密码
     * @param method 请求方式 后续不同请求方式 例如 DESCRIBE
     * @param uri rtsp 地址
     * @param nonce DESCRIBE请求返回到nonce值
     * @return 验证信息response
     */
    public static String calculateDigestResponse(String username, String realm, String password,
                                                 String method, String uri, String nonce) {
        try {
            String ha1 = calculateMD5(username + ":" + realm + ":" + password);
            String ha2 = calculateMD5(method + ":" + uri);
            return calculateMD5(ha1 + ":" + nonce + ":" + ha2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String socketAuthor(Socket socket ,String describeRequest) throws IOException {
        OutputStream outputStream = socket.getOutputStream();
        InputStream inputStream = socket.getInputStream();
        outputStream.write(describeRequest.getBytes());
        byte[] buffer = new byte[1024];
        int bytesRead = inputStream.read(buffer);
        outputStream.flush();
        return new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
    }

    /**
     * digest认证方式一 计算response ANSI
     * response = md5(password:nonce:md5(public_method:url))
     * @param username 设备用户名
     * @param password 设备密码
     * @return
     */
    public static String getAuthorization(String username, String password) {
        String credentials = username + ":" + password;
        return java.util.Base64.getEncoder().encodeToString(credentials.getBytes());
    }

    /**
     * 字符串转md5
     * @param input 字符串
     * @return md5
     */
    private static String calculateMD5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hashedBytes = md.digest(input.getBytes());
            // 将哈希后的字节数组转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : hashedBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 正则匹配字符串中的内容
     * @param input 字符串
     * @param regex 正则表达式
     * @return 符合正则表达式的字符串
     */
    private static String extractValue(String input, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(input);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }


}

14、Opencv工具类

14.0、前置依赖

<properties>
        <javacpp.platform.android-arm>android-arm</javacpp.platform.android-arm>
        <javacpp.platform.android-arm64>android-arm64</javacpp.platform.android-arm64>
        <javacpp.platform.android-x86>android-x86</javacpp.platform.android-x86>
        <javacpp.platform.android-x86_64>android-x86_64</javacpp.platform.android-x86_64>
        <javacpp.platform.ios-arm>ios-arm</javacpp.platform.ios-arm>
        <javacpp.platform.ios-arm64>ios-arm64</javacpp.platform.ios-arm64>
        <javacpp.platform.ios-x86>ios-x86</javacpp.platform.ios-x86>
        <javacpp.platform.ios-x86_64>ios-x86_64</javacpp.platform.ios-x86_64>
        <javacpp.platform.linux-armhf>linux-armhf</javacpp.platform.linux-armhf>
        <javacpp.platform.linux-arm64>linux-arm64</javacpp.platform.linux-arm64>
        <javacpp.platform.linux-ppc64le>linux-ppc64le</javacpp.platform.linux-ppc64le>
        <javacpp.platform.linux-x86>linux-x86</javacpp.platform.linux-x86>
        <javacpp.platform.linux-x86_64>linux-x86_64</javacpp.platform.linux-x86_64>
        <javacpp.platform.macosx-x86_64>macosx-x86_64</javacpp.platform.macosx-x86_64>
        <javacpp.platform.windows-x86>windows-x86</javacpp.platform.windows-x86>
        <javacpp.platform.windows-x86_64>windows-x86_64</javacpp.platform.windows-x86_64>
</properties>
<!-- ============ javacv ====================-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>1.5.10</version>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.10</version>
            <exclusions>
                <exclusion>
                    <artifactId>opencv</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>ffmpeg</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>openblas</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>librealsense2</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>leptonica</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>tesseract</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>artoolkitplus</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>librealsense</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>libfreenect</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>libdc1394</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>flycapture</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>libfreenect2</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>videoinput</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>javacpp-platform</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>opencv</artifactId>
            <version>4.9.0-1.5.10</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>openblas</artifactId>
            <version>0.3.26-1.5.10</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg</artifactId>
            <version>6.1.1-1.5.10</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>librealsense2</artifactId>
            <version>2.53.1-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>leptonica</artifactId>
            <version>1.84.1-1.5.10</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>tesseract</artifactId>
            <version>5.3.4-1.5.10</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>artoolkitplus</artifactId>
            <version>2.3.1-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>librealsense</artifactId>
            <version>1.12.4-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>libfreenect</artifactId>
            <version>0.5.7-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>libdc1394</artifactId>
            <version>2.2.6-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>flycapture</artifactId>
            <version>2.13.3.31-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--        只支持windows和linux-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>libfreenect2</artifactId>
            <version>0.2.0-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--        只支持windows-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>videoinput</artifactId>
            <version>0.200-1.5.9</version>
            <classifier>${javacpp.platform.windows-x86_64}</classifier>
            <exclusions>
                <exclusion>
                    <artifactId>javacpp</artifactId>
                    <groupId>org.bytedeco</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- ============ javacv ====================-->

14.1、Mat图像处理工具类

package com.sdj.demo.opencvUtil;

import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.opencv.core.*;
import org.opencv.core.Point;
import org.opencv.features2d.AKAZE;
import org.opencv.features2d.BFMatcher;
import org.opencv.features2d.Features2d;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.InputStream;

import static org.opencv.core.Core.NORM_L2;
import static org.opencv.core.Core.log;

/**
 * javacv opencv 图片操作工具类
 */
public class ImageUtils {
    private static final Logger logger= LoggerFactory.getLogger(ImageUtils.class);
    /**
     * 去黑边"全黑"阈值
     */
    private static final double BLACK_VALUE = 5;
    /**
     * 去白边“全白”阈值
     */
    private static final double WHITE_VALUE = 254.0;
    /**
     * 相似度阈值
     */
    private static final double SIMILARITY_VALVE = 60.0;

    private ImageUtils() {
    }

    /**
     * 保存图片
     *
     * @param fileName 绝对路径
     * @param mat      源Mat
     */
    public static void save(String fileName, Mat mat) {
        Imgcodecs.imwrite(fileName, mat);
    }

    /**
     * 帧转换为Mat
     *
     * @param frame 帧
     * @return Mat数据
     */
    public static Mat convertToOrgOpenCvCoreMat(Frame frame) {
        return new OpenCVFrameConverter.ToOrgOpenCvCoreMat().convertToOrgOpenCvCoreMat(frame);
    }

    /**
     * 从视频中找出指定帧
     *
     * @return 指定帧, 如果找不到,返回null
     */
    public static Mat findMat(int frameIndex, String videoPath) {
        Mat retMat = null;
        Frame frame = findFrame(frameIndex, videoPath);
        if (frame != null) {
            retMat = convertToOrgOpenCvCoreMat(frame);
        }
        return retMat;
    }

    /**
     * 从视频中找出指定帧
     *
     * @return 指定帧, 如果找不到,返回null
     */
    public static Frame findFrame(int frameIndex, String videoPath) {
        Frame frame = null;
        try (FFmpegFrameGrabber fFmpegFrameGrabber = new FFmpegFrameGrabber(videoPath)) {
            fFmpegFrameGrabber.start();
            fFmpegFrameGrabber.setFrameNumber(frameIndex);
            Frame srcFrame = fFmpegFrameGrabber.grabImage();
            if (srcFrame != null) {
                frame = srcFrame.clone();
            }
        } catch (FrameGrabber.Exception e) {
            logger.error("从视频中找出指定帧失败",e);
        }
        return frame;
    }

    /**
     * 从rtsp流中抓拍一帧数据
     * @return 指定帧, 如果找不到,返回null
     */
    public static Frame findRtspFrame(String rtspPath) {
        Frame frame = null;
        try (FFmpegFrameGrabber fFmpegFrameGrabber = new FFmpegFrameGrabber(rtspPath)) {
            fFmpegFrameGrabber.start();
            Frame srcFrame = fFmpegFrameGrabber.grabImage();
            if (srcFrame != null) {
                frame = srcFrame.clone();
            }
        } catch (FrameGrabber.Exception e) {
            logger.error("从rtsp流中抓拍一帧数据失败",e);
        }
        return frame;
    }

    /**
     * 显示图片, 阻塞。默认标题“结果”
     *
     * @param mat Mat
     */
    public static void showImg(Mat mat) {
        showImg(mat, "结果", 0);
    }

    /**
     * 显示图片,阻塞指定时长。默认标题“结果”
     *
     * @param mat    Mat
     * @param second int
     */
    public static void showImg(Mat mat, int second) {
        showImg(mat, "结果", second);
    }

    /**
     * 显示图片与标题,阻塞指定时长
     *
     * @param mat    Mat
     * @param title  标题
     * @param second int
     */
    public static void showImg(Mat mat, String title, int second) {
        HighGui.imshow(title, mat);
        HighGui.waitKey(second * 1000);
    }


    /**
     * 按照指定的尺寸截取Mat,截取宽高自动计算(对称)。坐标原点为左上角
     *
     * @param src 源Mat
     * @param x   x
     * @param y   y
     * @return 截取后的Mat
     */
    public static Mat cut(Mat src, int x, int y) {
        // 截取尺寸
        int width = src.width() - 2 * x;
        int height = src.height() - 2 * y;
        return cut(src, x, y, width, height);
    }

    /**
     * 按照指定的尺寸截取Mat,坐标原点为左上角
     *
     * @param src    源Mat
     * @param x      x
     * @param y      y
     * @param width  width
     * @param height height
     * @return 截取后的Mat
     */
    public static Mat cut(Mat src, int x, int y, int width, int height) {
        if (x < 0) {
            x = 0;
        }
        if (y < 0) {
            y = 0;
        }
        if (width > src.width()) {
            width = src.width();
        }
        if (height > src.height()) {
            height = src.height();
        }
        // 截取尺寸
        Rect rect = new Rect(x, y, width, height);
        return new Mat(src, rect);
    }

    /**
     * 缩放
     *
     * @param srcMat 源Mat
     * @param dSize  目标大小
     * @return 缩放结果Mat
     */
    public static Mat resize(Mat srcMat, Size dSize) {
        Mat retMat = new Mat();
        Imgproc.resize(srcMat, retMat, dSize);
        return retMat;
    }

    /**
     * 逆时针旋转,但是图片宽高不用变,此方法与rotateLeft和rotateRight不兼容
     *
     * @param src    源
     * @param angele 旋转的角度
     * @return 旋转后的对象
     */
    public static Mat rotate(Mat src, double angele) {
        Mat dst = src.clone();
        Point center = new Point(src.width() / 2.0, src.height() / 2.0);
        Mat affineTrans = Imgproc.getRotationMatrix2D(center, angele, 1.0);
        Imgproc.warpAffine(src, dst, affineTrans, dst.size(), Imgproc.INTER_NEAREST);
        return dst;
    }

    /**
     * 图像整体向左旋转90度
     *
     * @param src Mat
     * @return 旋转后的Mat
     */
    public static Mat rotateLeft(Mat src) {
        Mat tmp = new Mat();
        // 此函数是转置、(即将图像逆时针旋转90度,然后再关于x轴对称)
        Core.transpose(src, tmp);
        Mat result = new Mat();
        // flipCode = 0 绕x轴旋转180, 也就是关于x轴对称
        // flipCode = 1 绕y轴旋转180, 也就是关于y轴对称
        // flipCode = -1 此函数关于原点对称
        Core.flip(tmp, result, 0);
        return result;
    }

    /**
     * 图像整体向左旋转90度
     *
     * @param src Mat
     * @return 旋转后的Mat
     */
    public static Mat rotateRight(Mat src) {
        Mat tmp = new Mat();
        // 此函数是转置、(即将图像逆时针旋转90度,然后再关于x轴对称)
        Core.transpose(src, tmp);
        Mat result = new Mat();
        // flipCode = 0 绕x轴旋转180, 也就是关于x轴对称
        // flipCode = 1 绕y轴旋转180, 也就是关于y轴对称
        // flipCode = -1 此函数关于原点对称
        Core.flip(tmp, result, 1);
        return result;
    }

    /**
     * 灰度处理 BGR灰度处理
     *
     * @param src 原图Mat
     * @return Mat 灰度后的Mat
     */
    public static Mat gray(Mat src) {
        Mat gray = new Mat();
        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
        return gray;
    }

    /**
     * 去除图片黑边,若无黑边,则原图副本返回。默认“全黑”阈值为 {@code BLACK_VALUE}
     *
     * @param srcMat 预去除黑边的Mat
     * @return 去除黑边之后的Mat
     */
    public static Mat removeBlackEdge(Mat srcMat) {
        return removeBlackEdge(srcMat, BLACK_VALUE);
    }

    /**
     * 去除图片黑边,若无黑边,则原图副本返回。
     *
     * @param blackValue 一般低于5的已经是很黑的颜色了
     * @param srcMat     源Mat对象
     * @return Mat对象
     */
    public static Mat removeBlackEdge(Mat srcMat, double blackValue) {
        // 预截取,默认播放条等情况的处理
        Mat smallMat = cut(srcMat, (int) (srcMat.width() * 0.02), (int) (srcMat.height() * 0.02));
        // 灰度
        Mat grayMat = gray(smallMat);
        int topRow = 0;
        int leftCol = 0;
        int rightCol = grayMat.width() - 1;
        int bottomRow = grayMat.height() - 1;

        // 上方黑边判断
        for (int row = 0; row < grayMat.height(); row++) {
            // 判断当前行是否基本“全黑”,阈值自定义;
            if (sum(grayMat.row(row)) / grayMat.width() < blackValue) {
                // 更新截取条件
                topRow = row;
            } else {
                break;
            }
        }
        // 左边黑边判断
        for (int col = 0; col < grayMat.width(); col++) {
            // 判断当前列是否基本“全黑”,阈值自定义;
            if (sum(grayMat.col(col)) / grayMat.height() < blackValue) {
                // 更新截取条件
                leftCol = col;
            } else {
                break;
            }
        }
        // 右边黑边判断
        for (int col = grayMat.width() - 1; col > 0; col--) {
            // 判断当前列是否基本“全黑”,阈值自定义;
            if (sum(grayMat.col(col)) / grayMat.height() < blackValue) {
                // 更新截取条件
                rightCol = col;
            } else {
                break;
            }
        }
        // 下方黑边判断
        for (int row = grayMat.height() - 1; row > 0; row--) {
            // 判断当前行是否基本“全黑”,阈值自定义;
            if (sum(grayMat.row(row)) / grayMat.width() < blackValue) {
                // 更新截取条件
                bottomRow = row;
            } else {
                break;
            }
        }

        int x = leftCol;
        int y = topRow;
        int width = rightCol - leftCol;
        int height = bottomRow - topRow;

        if (leftCol == 0 && rightCol == grayMat.width() - 1 && topRow == 0 && bottomRow == grayMat.height() - 1) {
            return srcMat.clone();
        }
        return cut(smallMat, x, y, width, height);
    }

    /**
     * 求和
     *
     * @param mat mat
     * @return sum
     */
    private static double sum(Mat mat) {
        double sum = 0;
        for (int row = 0; row < mat.height(); row++) {
            for (int col = 0; col < mat.width(); col++) {
                sum += mat.get(row, col)[0];
            }
        }
        return sum;
    }

    /**
     * 去除图片白边(纯色,灰度值为255.0),若无白边,则原图副本返回。
     *
     * @param srcMat 源Mat对象
     * @return Mat对象
     */
    public static Mat removeWhiteEdge(Mat srcMat) {
        return removeWhiteEdge(srcMat, WHITE_VALUE);
    }

    /**
     * 去除图片白边(纯色,灰度值为255.0),若无白边,则原图副本返回。
     *
     * @param srcMat     源Mat对象
     * @param whiteValue 一般高于254.0的已经是很白的颜色了
     * @return Mat对象
     */
    public static Mat removeWhiteEdge(Mat srcMat, double whiteValue) {
        // 灰度
        Mat grayMat = gray(srcMat);
        int topRow = 0;
        int leftCol = 0;
        int rightCol = grayMat.width() - 1;
        int bottomRow = grayMat.height() - 1;

        // 上方白边判断
        for (int row = 0; row < grayMat.height(); row++) {
            // 判断当前行是否全白
            if (sum(grayMat.row(row)) / grayMat.width() > whiteValue) {
                // 更新截取条件
                topRow = row;
            } else {
                break;
            }
        }
        // 左边白边判断
        for (int col = 0; col < grayMat.width(); col++) {
            // 判断当前列是否全白
            if (sum(grayMat.col(col)) / grayMat.height() > whiteValue) {
                // 更新截取条件
                leftCol = col;
            } else {
                break;
            }
        }
        // 右边白边判断
        for (int col = grayMat.width() - 1; col > 0; col--) {
            // 判断当前列是否基本全白
            if (sum(grayMat.col(col)) / grayMat.height() > whiteValue) {
                // 更新截取条件
                rightCol = col;
            } else {
                break;
            }
        }
        // 下方白边判断
        for (int row = grayMat.height() - 1; row > 0; row--) {
            // 判断当前行是否基本全白
            if (sum(grayMat.row(row)) / grayMat.width() > whiteValue) {
                // 更新截取条件
                bottomRow = row;
            } else {
                break;
            }
        }

        int x = leftCol;
        int y = topRow;
        int width = rightCol - leftCol;
        int height = bottomRow - topRow;

        if (leftCol == 0 && rightCol == grayMat.width() - 1 && topRow == 0 && bottomRow == grayMat.height() - 1) {
            return srcMat.clone();
        }
        return cut(srcMat, x, y, width, height);
    }

    /**
     * 直方图均衡化,入参必须为灰度图
     *
     * @param grayMat 灰度图
     * @return 均衡化后的Mat
     * @throws IllegalArgumentException 如果入参不是灰度图
     */
    public static Mat equalizeHist(Mat grayMat) {
        if (grayMat.channels() != 1) {
            throw new IllegalArgumentException("入参必须为灰度图");
        }
        Mat retMat = new Mat();
        Imgproc.equalizeHist(grayMat, retMat);
        return retMat;
    }

    /**
     * 判断两个mat是否符合系统设定的阈值 SIMILARITY_VALVE
     * 采用 AKAZE 算法,后续可扩展其他算法
     * 默认resize尺寸为 src.width -> 512.0 然后按照 4:3 得到src的width,height,然后dest同尺度resize
     * 若去黑边 {@code ImageUtils#removeBlackEdge}
     *
     * @param src  源
     * @param dest 目标
     * @return boolean true 实际相似度 >= SIMILARITY_VALVE 返回true
     */
    public static boolean isSimilar(Mat src, Mat dest) {
        return isSimilar(src, dest, 512.0);

    }

    /**
     * 判断两个mat是否符合系统设定的阈值 SIMILARITY_VALVE
     * 采用 AKAZE 算法,后续可扩展其他算法
     * 默认resize尺寸为 src.width -> width 然后按照 4:3 得到src的width,height,然后dest同尺度resize
     * 若去黑边 {@code ImageUtils#removeBlackEdge}
     *
     * @param src   源
     * @param dest  目标
     * @param width 宽
     * @return boolean true 实际相似度 >= SIMILARITY_VALVE 返回true
     */
    public static boolean isSimilar(Mat src, Mat dest, double width) {
        double height = width * 3 / 4;
        return isSimilar(src, dest, width, height, SIMILARITY_VALVE);

    }

    /**
     * 判断两个mat是否符合系统设定的阈值 SIMILARITY_VALVE
     * 采用 AKAZE 算法,后续可扩展其他算法
     * 给啥就比啥
     *
     * @param src             源
     * @param dest            目标
     * @param width           图片宽(px)
     * @param height          图片高(px)
     * @param similarityValue 相似度阈值
     * @return boolean true 实际相似度 >= SIMILARITY_VALVE 返回true
     */
    public static boolean isSimilar(Mat src, Mat dest, double width, double height, double similarityValue) {
        boolean isSimilar = false;
        // 缩放
        Size size = new Size(width, height);
        Mat smallSrcMat = resize(src, size);
        Mat smallDestMat = resize(dest, size);

        Mat srcGrayMat = gray(smallSrcMat);
        Mat destGrayMat = gray(smallDestMat);

        AKAZE akaze = AKAZE.create();

        MatOfKeyPoint mokp = new MatOfKeyPoint();
        Mat desc = new Mat();
        akaze.detect(srcGrayMat, mokp);
        akaze.compute(srcGrayMat, mokp, desc);

        MatOfKeyPoint mokp2 = new MatOfKeyPoint();
        Mat desc2 = new Mat();
        akaze.detect(destGrayMat, mokp2);
        akaze.compute(destGrayMat, mokp2, desc2);

        BFMatcher matcher = BFMatcher.create(NORM_L2, true);
        MatOfDMatch matOfDMatch = new MatOfDMatch();
        matcher.match(desc, desc2, matOfDMatch);
        double similarity = (double) (2 * matOfDMatch.toArray().length) / (mokp.toArray().length + mokp2.toArray().length) * 100;
        if (similarity >= similarityValue) {
            isSimilar = true;
        }
        return isSimilar;
    }

    /**
     * 画出图片中的特征点,默认采用 AKAZE 算法
     *
     * @param imageMat 源图片
     * @return 画出特征点的 Mat
     */
    public static Mat drawKeyPoints(Mat imageMat) {
        AKAZE akaze = AKAZE.create();
        MatOfKeyPoint mokp = new MatOfKeyPoint();
        akaze.detect(imageMat, mokp);
        return drawKeyPoints(imageMat, mokp);
    }

    /**
     * 画出图片中的特征点
     *
     * @param imageMat 源图片
     * @param mokp     特征点
     * @return 画出特征点的 Mat
     */
    public static Mat drawKeyPoints(Mat imageMat, MatOfKeyPoint mokp) {
        Mat retMat = new Mat();
        Features2d.drawKeypoints(imageMat, mokp, retMat);
        return retMat;
    }

    private static void drawPoint(Mat srcMat, MatOfKeyPoint mokp, double width, double height, boolean src) {
        Mat outMat = new Mat();
        Features2d.drawKeypoints(srcMat, mokp, outMat, new Scalar(0, 0, 255));
        if (src) {
            save("/tmp/tmp/" + width + " X " + height + "_src.jpg", outMat);
        } else {

            save("/tmp/tmp/" + width + " X " + height + "_dest.jpg", outMat);
        }
    }


    /**
     * 未处理
     * 反色处理
     *
     * @param image
     * @return
     */
    public static Mat inverse(Mat image) {
        int width = image.cols();
        int height = image.rows();
        int dims = image.channels();
        byte[] data = new byte[width * height * dims];
        image.get(0, 0, data);
        int index = 0;
        int r = 0, g = 0, b = 0;
        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width * dims; col += dims) {
                index = row * width * dims + col;
                b = data[index] & 0xff;
                g = data[index + 1] & 0xff;
                r = data[index + 2] & 0xff;
                r = 255 - r;
                g = 255 - g;
                b = 255 - b;
                data[index] = (byte) b;
                data[index + 1] = (byte) g;
                data[index + 2] = (byte) r;
            }
        }
        image.put(0, 0, data);
        return image;
    }


    /**
     * Mat转换成BufferedImage
     *
     * @param img     要转换的Mat
     * @param extName 格式为 ".jpg", ".png", etc
     * @return BufferedImage
     */
    public static BufferedImage mat2BufferedImage(Mat img, String extName) {
        // 将矩阵转换为适合此文件扩展名的字节矩阵
        MatOfByte mob = new MatOfByte();
        Imgcodecs.imencode(extName, img, mob);
        // 将字节矩阵转换为字节数组
        byte[] byteArray = mob.toArray();
        BufferedImage bufImage = null;
        try (InputStream in = new ByteArrayInputStream(byteArray)) {
            bufImage = ImageIO.read(in);
        } catch (Exception e) {
            logger.error("mat2BufferedImage error",e);
        }
        return bufImage;
    }

    /**
     * BufferedImage转换成Mat
     *
     * @param original 要转换的BufferedImage
     * @param imgType  bufferedImage的类型 如 BufferedImage.TYPE_3BYTE_BGR
     * @param matType  转换成mat的type 如 CvType.CV_8UC3
     */
    public static Mat BufImg2Mat(BufferedImage original, int imgType, int matType) {
        if (original == null) {
            logger.error("original == null");
            return null;
        }
        // Don't convert if it already has correct type
        if (original.getType() != imgType) {

            // Create a buffered image
            BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), imgType);

            // Draw the image onto the new buffer
            Graphics2D g = image.createGraphics();
            try {
                g.setComposite(AlphaComposite.Src);
                g.drawImage(original, 0, 0, null);
            } finally {
                g.dispose();
            }
        }

        byte[] pixels = ((DataBufferByte) original.getRaster().getDataBuffer()).getData();
        Mat mat = Mat.eye(original.getHeight(), original.getWidth(), matType);
        mat.put(0, 0, pixels);
        return mat;
    }

}

14.2、将RTSP直播流读取片段为视频

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.*;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.opencv_core.IplImage;
import org.bytedeco.opencv.opencv_java;
import org.opencv.core.*;
import org.opencv.core.Point;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.springframework.util.CollectionUtils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * @author shendongjun
 * @project_name demo
 * @create 2023-12-05 10:32
 * @describe
 */
public class RtspVideo {

    static {
        Loader.load(opencv_java.class);
    }

    public static void main(String[] args) throws Exception {
        String url = "rtsp://192.168.1.72:8554/video/e-1920x1080-2M.mp4";
        String mp4 = "D:\\video.mp4";
        createRecord(url,mp4);
    }

    public static void createRecord(String rtspUrl,String mp4Path) {
        try {
            FFmpegLogCallback.set();
            FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtspUrl);
            grabber.start();
            FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4Path, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
            recorder.setFormat("mp4");
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setVideoBitrate(grabber.getVideoBitrate());
            recorder.start();
            LocalDateTime startTime = LocalDateTime.now();
            Frame frame;
            OpenCVFrameConverter.ToMat toMat=new OpenCVFrameConverter.ToMat();
            Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
            while (startTime.plusSeconds(20).compareTo(LocalDateTime.now()) > 0
                    && (frame = grabber.grab()) != null) {
                record(recorder,toMat,frame,java2DFrameConverter);
            }
            recorder.stop();
            grabber.stop();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void record(FFmpegFrameRecorder recorder, OpenCVFrameConverter.ToMat toMat,Frame frame,Java2DFrameConverter java2DFrameConverter) throws Exception{
        Mat mat = ImageUtils.convertToOrgOpenCvCoreMat(frame);
        if(mat==null)return;
        Mat resize = ImageUtils.resize(mat, new Size(640, 640));
        /*Mat mat1 = ImageUtils.rotateRight(mat);*/
        /*processRecord(mat);*/
        Frame frameEnd = toMat.convert(resize);
        recorder.record(frameEnd);
        frameEnd.close();
    }

    public static void processRecord(Mat mat) {
        /*Point center=new Point(1,2);
        int cSize = 2;//我想圈在大一点
        //对中心的圈圈点涂上白色
        Imgproc.circle(mat, center, cSize / 2, new Scalar(1,2,3), -1);*/
    }

}

14.3、rtsp实时预览

import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_java;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;

import javax.swing.*;
import java.awt.*;

/**
 * @author djshen
 * @date 2024/1/30 17:31
 * @des
 **/
public class RTSP实时预览 {
    static {
        Loader.load(opencv_java.class);
    }

    public static void main(String[] args) throws Exception {
        String rtsp = "rtsp://admin:xtkj12345@192.168.1.87:554/h264/ch33/main/av_stream";
        rtsp(rtsp);
    }
    public static void rtsp(String rtsp) throws FFmpegFrameGrabber.Exception {
        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtsp);
        grabber.setOption("rtsp_transport", "tcp"); // 使用tcp的方式,不然会丢包很严重
        grabber.setImageWidth(960);
        grabber.setImageHeight(540);
        grabber.start();
        CanvasFrame canvasFrame = new CanvasFrame("RTSP实时预览窗口");// 创建窗口
        canvasFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置窗口关闭程序是否也跟随关闭
        canvasFrame.setAlwaysOnTop(true);
        while (true)
        {
            Frame frame = grabber.grabImage();
            Mat mat = ImageUtils.convertToOrgOpenCvCoreMat(frame);
            if(mat==null || mat.empty() || !mat.isContinuous())continue;
            processRecord(mat);
            OpenCVFrameConverter.ToMat toMat=new OpenCVFrameConverter.ToMat();
            Frame frameEnd = toMat.convert(mat);
            toMat.close();
            canvasFrame.showImage(frameEnd);
            frameEnd.close();
            frame.close();
        }
    }
    /**
     * 绘制mat
     * @param mat
     */
    public static void processRecord(Mat mat) {
        /*int min = Math.min(HeightPx(), WidthPx());
        int cSize = (int) (Math.max(min / cSizePre, minCsize) * 1.5);//我想圈在大一点
        int n = id.length();
        int h = (int) Math.sqrt((double) (cSize * cSize) / (n * n + 1));
        double fontScale = Imgproc.getFontScaleFromHeight(Imgproc.FONT_HERSHEY_SIMPLEX, h, 3);
        org.opencv.core.Point textBegin = new Point(center.x - (double) (n * h) / 2, center.y + (double) h / 2);*/
        /*//对中心的圈圈点涂上白色
        Imgproc.circle(mat, center, cSize / 2, ColorSequence.convertToScalar(Color.WHITE), -1);
        //对序号进行渲染
        Imgproc.putText(mat, id, textBegin, Imgproc.FONT_HERSHEY_SIMPLEX, fontScale, scalar, 1);
        //对中心点画个圈
        Imgproc.circle(mat, center, cSize / 2, scalar, 3);
        //绘制矩形
        Imgproc.rectangle(mat, new Rect(coor.getCoorNorthwestLeftPx(), coor.getCoorNorthwestTopPx(), coor.getCoorWidthPx(), coor.getCoorHeightPx()), scalar, 3);*/

    }
}

15、位置计算

15.1、判断点在线段的上下侧

/* 小于0在线段上面 大于0在线段下面 */
public static boolean isPointOnTopOfHorizontalLine(List<RgPoint> points, RgPoint point) {
        points = points.stream().sorted(Comparator.comparing(RgPoint::getPointX)).collect(Collectors.toList());
        int Ax=points.get(0).getPointX();
        int Ay=points.get(0).getPointY();
        int Bx=points.get(1).getPointX();
        int By=points.get(1).getPointY();
        int Px=point.getPointX();
        int Py=point.getPointY();
        int crossProduct = (Bx - Ax) * (Py - Ay) - (By - Ay) * (Px - Ax);
        return crossProduct < 0;
}

15.2、判断点是否在区域内

public static boolean isPointInPolygon(List<RgPoint> points, RgPoint rgPoint){
        Polygon polygon = new Polygon();
        for (RgPoint point : points) {
            polygon.addPoint(point.getPointX(), point.getPointY());
        }
        return polygon.contains(rgPoint.getPointX(), rgPoint.getPointY());
}

15.3、计算区域重叠面积

private double overlap(YoloCoor yoloCoor, List<RgPoint> points) {
        //获取检出目标对象
        GeometryFactory geometryFactory = new GeometryFactory();
        Coordinate yc1=new Coordinate(yoloCoor.getCoorNorthwestLeftPx()*1.0d,yoloCoor.getCoorNorthwestTopPx());
        Coordinate yc2=new Coordinate(yoloCoor.getCoorNorthwestLeftPx()*1.0d+yoloCoor.getCoorWidthPx(),yoloCoor.getCoorNorthwestTopPx());
        Coordinate yc3=new Coordinate(yoloCoor.getCoorNorthwestLeftPx()*1.0d+yoloCoor.getCoorWidthPx(),yoloCoor.getCoorNorthwestTopPx()+yoloCoor.getCoorHeightPx());
        Coordinate yc4=new Coordinate(yoloCoor.getCoorNorthwestLeftPx()*1.0d,yoloCoor.getCoorNorthwestTopPx()+yoloCoor.getCoorHeightPx());
        Coordinate[] yCoordinates={yc1,yc2,yc3,yc4,yc1};
        Polygon p1=geometryFactory.createPolygon(yCoordinates);
        //获取区域目标对象
        List<Coordinate> pCoordinateList=new ArrayList<>();
        for (RgPoint point : points) {
            pCoordinateList.add(new Coordinate(point.getPointX(),point.getPointY()));
        }
        Coordinate[] pCoordinates = pCoordinateList.toArray(new Coordinate[0]);
        Polygon p2=geometryFactory.createPolygon(pCoordinates);
        //计算两对象直接重叠面积
        OverlayOp op = new OverlayOp(p1,p2);
        Geometry g = op.getResultGeometry(OverlayOp.INTERSECTION);
        if(g.getArea()>0){
            return g.getArea();
        }
        return 0;
    }

15.4 计算两个线段交点

public static LabelPoint getIntersection(Ponint p1, Ponint p2, Ponint p3, Ponint p4) {
        double x1 = p1.getPointX();
        double y1 = p1.getPointY();
        double x2 = p2.getPointX();
        double y2 = p2.getPointY();
        double x3 = p3.getPointX();
        double y3 = p3.getPointY();
        double x4 = p4.getPointX();
        double y4 = p4.getPointY();
        double denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
        if (denominator == 0) {
            return null; // 平行线或重合线段没有交点
        }
        double intersectX = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denominator;
        double intersectY = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denominator;

        // Check if the intersection point is within the line segments
        if (intersectX < Math.min(x1, x2) || intersectX > Math.max(x1, x2) ||
                intersectX < Math.min(x3, x4) || intersectX > Math.max(x3, x4) ||
                intersectY < Math.min(y1, y2) || intersectY > Math.max(y1, y2) ||
                intersectY < Math.min(y3, y4) || intersectY > Math.max(y3, y4)) {
            return null;
        }

        return new Ponint((int) intersectX, (int) intersectY);
    }

16、LMDB数据库

16.0、前置依赖

<!-- https://mvnrepository.com/artifact/org.lmdbjava/lmdbjava -->
<dependency>
    <groupId>org.lmdbjava</groupId>
    <artifactId>lmdbjava</artifactId>
    <version>0.9.0</version>
</dependency>

16.1、操作工具类

import org.lmdbjava.Cursor;
import org.lmdbjava.Dbi;
import org.lmdbjava.Env;
import org.lmdbjava.Txn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Integer.BYTES;
import static java.nio.ByteBuffer.allocateDirect;
import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL;
import static org.lmdbjava.DbiFlags.MDB_CREATE;
import static org.lmdbjava.Env.create;
import static org.lmdbjava.Env.open;

/**
 *  Lmdb操作工具类
 */
@Component
public class LmdbService {
    private static final Logger logger= LoggerFactory.getLogger(LmdbService.class);
    /**
     * lmdb path
     */
    @Value("${lmdb.path}")
    private String lmdbPath;
    /**
     * env
     */
    private Env<ByteBuffer> env;
     /**
     * lmdb max size (MB)
     */
    @Value("${lmdb.max.size:100}")
    private int lmdbMaxSize;
    /**
     * lmdb库的数量
     */
    @Value("${lmdb.database.count}")
    private int lmdbDatabaseCount;

    /**
     * Description: 初始化操作 <br>
	 * @author jack<br>
	 * @version 1.0<br>
	 * @CreateDate 2020年5月11日 <br>
	 */
    @PostConstruct
    private void init(){
        try {
            final File path = new File(lmdbPath);
            if(path.exists()){
                env= open(path,lmdbMaxSize);
            }else {
                env = create(PROXY_OPTIMAL)
                        .setMapSize((long) lmdbMaxSize * 1024 * 1024)
                        .setMaxDbs(lmdbDatabaseCount)
                        .setMaxReaders(lmdbDatabaseCount*2)
                        .open(path);
            }
            logger.info("初始化Lmdb成功......");
        } catch (Exception e) {
            logger.error("Lmdb IO failure",e);
        }
    }

    public static String db1="db1";

    /**
     * Description: 向库中插入数据 
	 */
    public void putValueToDb(String dbName, String key,String value) {
        final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
        try (Txn<ByteBuffer> txn = env.txnWrite()) {
            final Cursor<ByteBuffer> c = db.openCursor(txn);
            c.put(bb(key), bb(value));
            c.close();
            txn.commit();
        }
    }

    /**
     * Description: 向库中插入数据 
     */
    public void putValueToDb(String dbName, String key,byte[] value) {
        final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
        try (Txn<ByteBuffer> txn = env.txnWrite()) {
            final Cursor<ByteBuffer> c = db.openCursor(txn);
            c.put(bb(key), bb(value));
            c.close();
            txn.commit();
        }
    }

    /**
     * Description: 获取库下所有的数据
	 */
    public Map<String, String> getAllValueByDbName(String dbName) {
        final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
        Map<String, String> map = new HashMap<>();
        try (Txn<ByteBuffer> txn = env.txnRead()) {
            Cursor<ByteBuffer> cursor = db.openCursor(txn);
            while (cursor.next()) {
                ByteBuffer key = cursor.key();
                ByteBuffer val = cursor.val();
                byte[] k = new byte[key.capacity()];
                byte[] v = new byte[val.capacity()];
                key.get(k);
                val.get(v);
                map.put(new String(k,StandardCharsets.UTF_8), new String(v,StandardCharsets.UTF_8));
            }
            cursor.close();
        }
        return map;
    }

    /**
     * Description: 获取库下所有的数据
     */
    public Map<String, byte[]> getAllByteValueByDbName(String dbName) {
        final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
        Map<String, byte[]> map = new HashMap<>();
        try (Txn<ByteBuffer> txn = env.txnRead()) {
            Cursor<ByteBuffer> cursor = db.openCursor(txn);
            while (cursor.next()) {
                ByteBuffer key = cursor.key();
                ByteBuffer val = cursor.val();
                byte[] k = new byte[key.capacity()];
                byte[] v = new byte[val.capacity()];
                key.get(k);
                val.get(v);
                map.put(new String(k,StandardCharsets.UTF_8), v);
            }
            cursor.close();
        }
        return map;
    }

    /**
     * Description: 根据dbname获取该库下的数量
	 */
    public long getDbCount(String dbName) {
        long size=0;
        final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
        try (Txn<ByteBuffer> txn = env.txnRead()) {
            Cursor<ByteBuffer> cursor = db.openCursor(txn);
            while (cursor.next()) {
                size++;
            }
            cursor.close();
        }
        return size;
    }

	/**
     * Description: 删除数据库指定key
	 */
	public boolean deleteValueFromDb(String dbName, String key) {
	    final Dbi<ByteBuffer> db = env.openDbi(dbName, MDB_CREATE);
	    boolean result = false;
	    try (Txn<ByteBuffer> txn = env.txnWrite()) {
	        ByteBuffer keyBuffer = bb(key);
	        result = db.delete(txn, keyBuffer);
	        txn.commit();
	    } catch (Exception e) {
	        logger.error("Failed to delete key: {} from database: {}", key, dbName, e);
	    }
	    return result;
	}

    /**
     * Description: int 格式化 ByteBuffer
	 */
    static ByteBuffer bb(final int value) {
        final ByteBuffer bb = allocateDirect(BYTES);
        bb.putInt(value).flip();
        return bb;
    }

    /**
     * Description: String 格式化 ByteBuffer
	 */
    static ByteBuffer bb(final String value) {
        byte[] val = value.getBytes(StandardCharsets.UTF_8);
        final ByteBuffer bb = allocateDirect(val.length);
        bb.put(val).flip();
        return bb;
    }

    /**
     * Description: byte[] 格式化 ByteBuffer
     */
    static ByteBuffer bb(final byte[] value) {
        final ByteBuffer bb = allocateDirect(value.length);
        bb.put(value).flip();
        return bb;
    }
}

文章作者:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 君の小站
编程 springboot java 常更新
喜欢就支持一下吧