SpringBoot+modbus4j实现ModebusTCP通讯定时读取多个plc设备数并存储进redis中

场景

SpringBoot+modbus4j实现ModebusTCP通讯读取数据:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/135292378

基于上面对单个PLC设备进行ModbusTCP通讯获取数据的过程。

业务开发中,如果需要定时读取多个设备的数据,且需要将设备数据存储进redis中缓存,以供其他业务功能使用。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi

实现

新建SpringBoot项目并添加需要的pom依赖

复制代码
        <!--modbus4j 依赖-->
        <dependency>
            <groupId>com.infiniteautomation</groupId>
            <artifactId>modbus4j</artifactId>
            <version>3.0.3</version>
        </dependency>

其他业务依赖

复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- redis 缓存操作 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
            <!-- 排除SpringBoot的版本管理 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--modbus4j 依赖-->
        <dependency>
            <groupId>com.infiniteautomation</groupId>
            <artifactId>modbus4j</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.6</version>
        </dependency>

配置文件中新增多个PLC设备相关信息,这里是三个,只关注必要连接相关信息,其他冗余信息忽略:

复制代码
#plc相关配置
modbus:
  task:
    rate: "10000"  # 定时任务执行周期
    timeout: 3000  # 读取超时(ms)
  # PLC寄存器配置
  devices:
    - ip: 127.0.0.1
      port: 502
      slaveId: 1
      name: "#1"
      registers:
        runSignal:
          address: "400153.0"  # V152.0运行信号
          type: "bool"
        pressure:
          address: "400201"    # VD200运行压力
          type: "float32"

      cameras:
        recognition:
          ip: "127.0.0.1"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 1 
          userId: 1 

        capture:
          ip: "127.0.0.1"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 2
          userId: 2 
    - ip: 127.0.0.2
      port: 502
      slaveId: 2
      name: "#2"
      registers:
        runSignal:
          address: "400154.0"  # V153.0运行信号
          type: "bool"
        pressure:
          address: "400202"    # VD201运行压力
          type: "float32"
      cameras:
        recognition:
          ip: "127.0.001"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 1 
          userId: 3 
        capture:
          ip: "127.0.0.1"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 2
          userId: 4 
    - ip: 127.0.0.3
      port: 502
      slaveId: 3
      name: "#3"
      registers:
        runSignal:
          address: "400154.0"  # V153.0运行信号
          type: "bool"
        pressure:
          address: "400202"    # VD201运行压力
          type: "float32"
      cameras:
        recognition:
          ip: "127.0.0.1"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 1 
          userId: 5 
        capture:
          ip: "127.0.0.1"
          port: 8000
          username: "admin"
          password: "123456"
          channel: 2
          userId: 6 

增加对应的配制类:

复制代码
package com.badao.demo.config;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@ConfigurationProperties(prefix = "modbus")
@Configuration
@Getter
@Setter
@Component
public class ModbusConfig {

    private transient Map<String, DeviceConfig> recognitionIpToDeviceMap;

    @PostConstruct
    public void initMapping() {
        // 初始化IP映射关系
        recognitionIpToDeviceMap = devices.stream()
                .filter(device -> device.getCameras()  != null)
                .filter(device -> device.getCameras().getRecognition()  != null)
                .collect(Collectors.toMap(
                        device -> device.getCameras().getRecognition().getIp(),
                        Function.identity()
                ));

        System.out.println("已初始化摄像头映射关系"+recognitionIpToDeviceMap);
    }

    public DeviceConfig findDeviceByRecognitionIp(String recognitionIp) {
        return recognitionIpToDeviceMap.get(recognitionIp);
    }

    private  TaskConfig task;
    private  List<DeviceConfig> devices; // 改为List接收

    @Data
    public static class TaskConfig {
        private String rate;
        private int timeout;
    }

    @Data
    public static class DeviceConfig {
        private String ip;       // 设备IP地址
        private int port;        // 端口号
        private int slaveId;     // 从站ID
        private String name;     // 设备名称
        private Map<String, RegisterConfig> registers;
        // 摄像头绑定配置
        private CameraBinding cameras;


        @Data
        public static class CameraBinding {
            private CameraConfig recognition;
            private CameraConfig capture;
        }

        @Data
        public static class CameraConfig {
            private String ip;
            private int port;
            private String username;
            private String password;
            private int channel;
            private int userId ;
        }
    }

    @Data
    public static class RegisterConfig {
        private String address;  // 寄存器地址(格式:400153.0 或 400201)
        private String type;     // 数据类型(bool/float32/int16等)
    }
}

新建modbus工具类

复制代码
package com.badao.demo.utils;

import com.serotonin.modbus4j.BatchRead;
import com.serotonin.modbus4j.BatchResults;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.locator.BaseLocator;

public class Modbus4jUtils {

    /**
     * 工厂。
     */
    static ModbusFactory modbusFactory;

    static {
        if (modbusFactory == null) {
            modbusFactory = new ModbusFactory();
        }
    }

    /**
     * 获取master
     *
     * @return
     * @throws ModbusInitException
     */
    public static ModbusMaster getMaster(String ip,int port) throws ModbusInitException {
        IpParameters params = new IpParameters();
        params.setHost(ip);
        params.setPort(port);
        // modbusFactory.createRtuMaster(wapper); //RTU 协议
        // modbusFactory.createUdpMaster(params);//UDP 协议
        // modbusFactory.createAsciiMaster(wrapper);//ASCII 协议
        //true启用长连接
        ModbusMaster master = modbusFactory.createTcpMaster(params, true);// TCP 协议
        master.setTimeout(2000);   // 超时设置
        master.setRetries(1);      // 失败后重试1次
        master.init();
        return master;
    }

    /**
     * 读取[01 Coil Status 0x]类型 开关数据
     *
     * @param slaveId
     *            slaveId
     * @param offset
     *            位置
     * @return 读取值
     * @throws ModbusTransportException
     *             异常
     * @throws ErrorResponseException
     *             异常
     */
    public static Boolean readCoilStatus(ModbusMaster master,int slaveId, int offset)
            throws ModbusTransportException, ErrorResponseException {
        // 01 Coil Status
        BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, offset);
        Boolean value = master.getValue(loc);
        return value;
    }

    /**
     * 读取[02 Input Status 1x]类型 开关数据
     *
     * @param slaveId
     * @param offset
     * @return
     * @throws ModbusTransportException
     * @throws ErrorResponseException
     */
    public static Boolean readInputStatus(ModbusMaster master,int slaveId, int offset)
            throws ModbusTransportException, ErrorResponseException {
        // 02 Input Status
        BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, offset);
        Boolean value = master.getValue(loc);
        return value;
    }

    /**
     * 读取[03 Holding Register类型 2x]模拟量数据
     *
     * @param slaveId
     *            slave Id
     * @param offset
     *            位置
     * @param dataType
     *            数据类型,来自com.serotonin.modbus4j.code.DataType
     * @return
     * @throws ModbusTransportException
     *             异常
     * @throws ErrorResponseException
     *             异常
     */
    public static Number readHoldingRegister(ModbusMaster master,int slaveId, int offset, int dataType)
            throws ModbusTransportException, ErrorResponseException {
        // 03 Holding Register类型数据读取
        BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
        Number value = master.getValue(loc);
        return value;
    }

    /**
     * 读取[04 Input Registers 3x]类型 模拟量数据
     *
     * @param slaveId
     *            slaveId
     * @param offset
     *            位置
     * @param dataType
     *            数据类型,来自com.serotonin.modbus4j.code.DataType
     * @return 返回结果
     * @throws ModbusTransportException
     *             异常
     * @throws ErrorResponseException
     *             异常
     */
    public static Number readInputRegisters(ModbusMaster master,int slaveId, int offset, int dataType)
            throws ModbusTransportException, ErrorResponseException {
        // 04 Input Registers类型数据读取
        BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, offset, dataType);
        Number value = master.getValue(loc);
        return value;
    }

    /**
     * 批量读取使用方法
     *
     * @throws ModbusTransportException
     * @throws ErrorResponseException
     * @throws ModbusInitException
     * @return
     */
    public static BatchResults<Integer> batchRead(ModbusMaster master,BatchRead<Integer> batchRead) throws ModbusTransportException, ErrorResponseException, ModbusInitException {
        // 是否连续请求
        batchRead.setContiguousRequests(false);
        BatchResults<Integer> results = master.send(batchRead);
        return results;
    }
}

新建定时任务读取数据

复制代码
package com.badao.demo.task;

import com.badao.demo.config.ModbusConfig;
import com.badao.demo.constants.Constants;
import com.badao.demo.entity.PlcData;
import com.badao.demo.utils.Modbus4jUtils;
import com.badao.demo.utils.RedisCache;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableScheduling
@Slf4j
@DependsOn("modbusConfig")  // 明确依赖配置类
public class GetModbusTCPDataTask {

    @Autowired
    private ModbusConfig modbusConfig;

    @Autowired
    private RedisCache redisCache;

    // 新增类成员变量(连接池)
    private final Map<String, ModbusMaster> masterPool = new ConcurrentHashMap<>();

    @Scheduled(fixedRateString = "2000")
    public void  getData() {
        modbusConfig.getDevices().parallelStream().forEach(device  -> {
            String deviceKey = device.getIp()  + ":" + device.getPort();
            ModbusMaster master = masterPool.computeIfAbsent(deviceKey,  k -> {
                try {
                    return createNewMaster(device);
                } catch (ModbusInitException e) {
                    throw new RuntimeException(e);
                }
            });
            try {
                if (!checkMasterValid(master)) {
                    masterPool.remove(deviceKey);
                    master = createNewMaster(device); // 重建连接
                }
                Number runSignal = Modbus4jUtils.readHoldingRegister(master, 1, 0, DataType.TWO_BYTE_INT_UNSIGNED);
                Number waterPress = Modbus4jUtils.readHoldingRegister(master, 1, 2, DataType.FOUR_BYTE_FLOAT);
                PlcData plcData = PlcData.builder()
                        .ip(device.getIp())
                        .runSignal(runSignal.intValue())
                        .waterPress(waterPress)
                        .build();
                redisCache.setCacheObject(Constants.PLC_KEY+device.getIp(),plcData,5, TimeUnit.SECONDS);

            } catch (ModbusTransportException e) {
                System.out.println(" 通信失败: 网络或端口不可达 - {}"+e.getMessage());
                // 发生通信错误时移除无效连接
                masterPool.remove(deviceKey);
                master.destroy();
            } catch (ErrorResponseException e) {
                //System.out.println(" 设备返回错误: 从站ID或寄存器地址无效 - {}"+e.getErrorResponse().getExceptionMessage());
                //System.out.println(e.getMessage());
            } catch (ModbusInitException e) {
                throw new RuntimeException(e);
            }
        });
    }



    private ModbusMaster createNewMaster(ModbusConfig.DeviceConfig device) throws ModbusInitException {
        try {
            return Modbus4jUtils.getMaster(device.getIp(),  device.getPort());
        } catch (ModbusInitException e) {
            throw new RuntimeException("Modbus初始化失败(ip): "+device.getIp() , e);
        }
    }
    //健康检查方法
    private boolean checkMasterValid(ModbusMaster master) {
        try {
            master.init();  // 尝试初始化验证连接状态
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    //在应用关闭时清理连接
    @PreDestroy
    public void cleanup() {
        masterPool.values().forEach(ModbusMaster::destroy);
    }
}

上面所用到的实体类

复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PlcData {

    private String  ip;
    private Number runSignal;
    private Number waterPress;

}

所用到的redis工具类

复制代码
package com.badao.demo.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * spring redis 工具类
 *
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public void deleteObject(String key)
    {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection
     */
    public void deleteObject(Collection collection)
    {
        redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
    {
        ListOperations listOperation = redisTemplate.opsForList();
        if (null != dataList)
        {
            int size = dataList.size();
            for (int i = 0; i < size; i++)
            {
                listOperation.leftPush(key, dataList.get(i));
            }
        }
        return listOperation;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(String key)
    {
        List<T> dataList = new ArrayList<T>();
        ListOperations<String, T> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);

        for (int i = 0; i < size; i++)
        {
            dataList.add(listOperation.index(key, i));
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(String key)
    {
        Set<T> dataSet = new HashSet<T>();
        BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     * @return
     */
    public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (null != dataMap)
        {
            for (Map.Entry<String, T> entry : dataMap.entrySet())
            {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(String key)
    {
        Map<String, T> map = redisTemplate.opsForHash().entries(key);
        return map;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

yml中redis相关配制

复制代码
# 数据源
spring:
  profiles:
    active: default  # 强制激活默认配置
  application:
    name: demo
  # redis 配置
  redis:
    # 地址
    #本地测试用
    host: 127.0.0.1
    port: 6379
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

所用的redis配制类

复制代码
package com.badao.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 *
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

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

Redis使用FastJson序列化类

复制代码
package com.badao.demo.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;

import java.nio.charset.Charset;

public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

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

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

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

运行效果:

相关推荐
打工的小王2 小时前
单例模式的实现
java·开发语言·单例模式
是宇写的啊2 小时前
单例模式-阻塞队列
java·开发语言·单例模式
u0104058362 小时前
Java中的单例模式详解
java·开发语言·单例模式
历程里程碑2 小时前
哈希1:两数之和:哈希表优化指南
java·开发语言·数据结构·c++·算法·哈希算法·散列表
小唐同学爱学习2 小时前
布隆过滤器
java·spring boot·中间件
码界奇点2 小时前
Tomcat与JDK版本对照全解析避坑指南生产环境选型建议
java·开发语言·容器·jdk·tomcat
Remember_9932 小时前
【数据结构】深入理解排序算法:从基础原理到高级应用
java·开发语言·数据结构·算法·spring·leetcode·排序算法
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Kafka消息队列场景解析
java·spring boot·面试·kafka·microservices·interview·distributed systems
qq_12498707532 小时前
基于Spring Boot的心理咨询预约微信小程序(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序·毕业设计