使用springboot-3.4.1搭建一个netty服务并且WebSocket消息通知(适用于设备直连操作,以及回复操作)

引入最新版本

<!--websocket-->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

启动类加入

//netty 协议服务端口启动
NettyTcpHandler.start();
package com.cqcloud.platform.handler;

import com.cqcloud.platform.service.IotMqttService;
import com.cqcloud.platform.service.impl.IotMqttServiceImpl;
import org.springframework.stereotype.Component;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2022年9月23日 🐬🐇 💓💕
 */
@Component
public class NettyTcpHandler {
    /**
     * IoT设备协议端口
     */
    private static int PORT = 1883;

    /**
     * 使用方法在启动类
     * 加上 NettyTcpHandler.start();
     * @throws Exception
     */
    public static void start() throws Exception {
        final NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        final NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 创建 IotPushService 实例
            IotMqttService iotPushService = new IotMqttServiceImpl();

            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ChannelPipeline pipeline = ch.pipeline();
                    // 添加处理器,处理所有连接的业务逻辑
                    pipeline.addLast(new TcpMqttServerHandler(iotPushService));
                }
            });
            // 绑定端口并启动
            ChannelFuture future = bootstrap.bind(PORT).sync();
            // 等待服务器关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭线程池
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

package com.cqcloud.platform.handler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import cn.hutool.json.JSONObject;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2022年10月06日 🐬🐇 💓💕
 */
@Slf4j
@Component
public class TcpEventHandler {

    // 使用 BiConsumer 来处理两个参数
    private final static Map<String, BiConsumer<String, String>> eventActions = new HashMap<>();

    public void registerEventAction(String eventCode, BiConsumer<String, String> action) {
        // 动态注册事件处理逻辑
        eventActions.put(eventCode, action);
    }

    public static void handleEvent(String evt, String imei, String reportContent) {
        // 根据事件类型找到对应的处理逻辑,并执行
        eventActions.getOrDefault(evt, TcpEventHandler::handleUnknownEvent).accept(imei, reportContent);
    }

    private static void handleUnknownEvent(String imei, String reportContent) {
        // 处理未知事件的逻辑
        System.out.println("imei: " + imei + ", 报告内容: " + reportContent);
    }

    public static void handleAlarm(String imei, String reportContent) {
        log.info("内容: {}", reportContent);
        // 获取目标用户列表(包括固定的用户 ID)
        List<String> targetUsers = buildTargetUsersList();
        // 构建消息并发送
        sendAlarmMessage(targetUsers, imei, reportContent);
    }


    private static List<String> buildTargetUsersList() {
        List<String> targetUsers = new ArrayList<>();
        targetUsers.add("1");
        targetUsers.add("2");
        //实际根据业务查询数据
        targetUsers.add("26967563820859392");
        return targetUsers;
    }

    private static void sendAlarmMessage(List<String> targetUsers, String imei, String reportContent) {
        // 将目标用户列表转换为逗号分隔的字符串
        String users = String.join(",", targetUsers);
        // 构建 JSON 消息
        JSONObject obj = new JSONObject();
        obj.set("imei", imei);
        obj.set("message", reportContent);
        obj.set("userId", users);
        // 发送消息
        WebSocketHandler.sendMessageToUser(users, obj.toString());
    }
}

package com.cqcloud.platform.handler;

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

import com.cqcloud.platform.service.IotMqttService;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * 物联网云平台设备协议
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2022年9月23日 🐬🐇 💓💕
 */
public class TcpMqttServerHandler extends SimpleChannelInboundHandler<ByteBuf>  {

	// 接口注入
	private final IotMqttService iotPushService;

	public TcpMqttServerHandler(IotMqttService iotPushService) {
		this.iotPushService = iotPushService;
	}
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
		byte[] byteArray;
		if (in.readableBytes() <= 0) {
			in.release();
			return;
		}
		byteArray = new byte[in.readableBytes()];
		in.readBytes(byteArray);
		if (byteArray.length <= 0) {
			in.release();
			return;
		}
		// 将消息传递给 iotPushService
		iotPushService.pushMessageArrived(byteArray);
		// 下发指令,假设返回的是多个指令
		List<String> externalValues = extractExternalValue("deviceId");
		// 转换为十六进制字符串
		String hexString = bytesToHex(byteArray);
		System.out.println("来自于物联网云平台设备协议的数据: " + hexString);
		// 使用 Optional 判断外部值
		// 使用 Optional 判断外部值,如果不为空则逐一处理每条指令
		Optional.ofNullable(externalValues).ifPresent(values -> {
			values.forEach(value -> {
				// 提取外部数据值并发送
				System.out.println("来自于物联网云平台设备协议的1883端口的提取外部数据值: " + value);
				sendResponse(ctx, value);
			});
		});
	}
	// 辅助方法:将字节数组转换为十六进制字符串
	private static String bytesToHex(byte[] bytes) {
		StringBuilder hexString = new StringBuilder();
		for (byte b : bytes) {
			String hex = Integer.toHexString(0xFF & b);
			if (hex.length() == 1) {
				hexString.append('0'); // 确保每个字节都为两位
			}
			hexString.append(hex);
		}
		return hexString.toString().toUpperCase(); // 返回大写格式
	}
	// 发送响应的统一辅助方法
	private void sendResponse(ChannelHandlerContext ctx, String hexResponse) {
		byte[] responseBytes = hexStringToByteArray(hexResponse);
		ByteBuf responseBuffer = Unpooled.copiedBuffer(responseBytes);
		ctx.writeAndFlush(responseBuffer);
	}
	// 将响应消息转换为字节数组
	public static byte[] hexStringToByteArray(String s) {
		int len = s.length();
		byte[] data = new byte[len / 2];
		for (int i = 0; i < len; i += 2) {
			data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
		}
		return data;
	}

	// 查询数据库当前设备号下需要下发的命令
	private List<String> extractExternalValue(String deviceId) {
		//这里自行查询数据库数据库,这里只模拟一个list集合
		ArrayList<Object> list = new ArrayList<>();
		// 如果记录不为空,获取最新记录的 externalValue
        list.stream().findFirst() // 获取最新的一条记录
			.map(latestRecord -> {
				// 处理最新记录的逻辑
				return ""; // 需要返回的值是 externalValue
			});// 获取最新的一条记录
        return null; // 如果没有找到,返回 null
	}
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		// 打印异常堆栈跟踪,便于调试和错误排查
		cause.printStackTrace();
		// 关闭当前的通道,释放相关资源
		ctx.close();
	}
}

package com.cqcloud.platform.handler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.springframework.stereotype.Component;

import cn.hutool.json.JSONUtil;
import io.micrometer.common.util.StringUtils;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;

/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2022年4月12日 🐬🐇 💓💕
 */
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocketHandler {

	public static synchronized int getOnlineCount() {
		return onlineCount;
	}

	public static synchronized void addOnlineCount() {
		WebSocketHandler.onlineCount++;
	}

	public static synchronized void subOnlineCount() {
		WebSocketHandler.onlineCount--;
	}
	// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
	private static int onlineCount = 0;

	// 根据名字存储websocket对象CopyOnWriteArraySet线程安全set,ConcurrentHashMap线程安全map
	public static Map<String, CopyOnWriteArraySet<WebSocketHandler>> webSocketMap = new ConcurrentHashMap<>();

	// 与某个客户端的连接会话,需要通过它来给客户端发送数据
	public Session session;

	// 心跳时间,长时间没心跳踢掉连接
	public long heartBeatTime;

	// 初次连接时间,用于控制连接时间过长,踢掉连接
	public long beginTime;

	/**
	 * 用户名称
	 */
	public String username;

	/**
	 * 发送消息
	 * @param username
	 * @param message
	 */
	public static void sendMessageToUser(String username, String message) {
		// 检查用户名是否在 map 中存在
		if (webSocketMap.containsKey(username)) {
			// 获取该用户的 WebSocketHandler 集合
			CopyOnWriteArraySet<WebSocketHandler> userHandlers = webSocketMap.get(username);

			// 遍历该用户的所有连接(每个用户可能有多个 WebSocket 连接)
			for (WebSocketHandler handler : userHandlers) {
				// 通过 WebSocketHandler 实例发送消息
				handler.sendMessageOne(message, username);
			}
		} else {
			System.out.println("并无在线用户: " + username);
		}
	}
	/**
	 * 连接建立成功调用的方法
	 */
	@OnOpen
	public void onOpen(@PathParam("username") String username, Session session) {
		this.username = username;
		this.session = session;
		this.heartBeatTime = System.currentTimeMillis();
		this.beginTime = System.currentTimeMillis();
		// 登陆用户必须按照用户id 格式登陆
		if (!"server".equals(username) && username.split(",").length < 3) {
			return;
		}
		// 将用户添加到websocket,支持单用户多出链接
		if (webSocketMap.containsKey(username)) {
			webSocketMap.get(username).add(this);
		} else {
			CopyOnWriteArraySet websocketSet = new CopyOnWriteArraySet();
			websocketSet.add(this);
			webSocketMap.put(username, websocketSet);
			addOnlineCount(); // 在线数加1
		}
		//注释掉 会退出
		Map<String, Object> messageMap = new ConcurrentHashMap<>();
		messageMap.put("type", "0");
		messageMap.put("message", username + "加入8000端口的的当前在线人数为" + getOnlineCount());
		messageMap.put("to", "all");
		messageMap.put("users", webSocketMap.keySet());
		messageMap.put("username", "server");
		sendMessageAll(JSONUtil.toJsonStr(messageMap));
	}
	/**
	 * 发送消息给所有用户
	 *
	 * @param message
	 * @throws IOException
	 */
	public void sendMessageAll(String message) {
		for (String key : webSocketMap.keySet()) {
			for (WebSocketHandler websocket : webSocketMap.get(key)) {
				websocket.session.getAsyncRemote().sendText(message);
			}
		}
	}
	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose() {
		if (StringUtils.isNotEmpty(this.username)) {
			try {
				if (this.session.isOpen()) {
					this.session.close();// 强制关闭
				}
				webSocketMap.get(username).remove(this);// 删除链接
				if (webSocketMap.get(username).isEmpty()) {
					webSocketMap.remove(username);
					subOnlineCount(); // 在线数减1
					// 刷新用户列表
					Map<String, Object> messageMap = new ConcurrentHashMap<>();
					messageMap.put("type", 0);
					messageMap.put("message", username + "退出!当前在线人数为" + getOnlineCount());
					messageMap.put("users", webSocketMap.keySet());
					sendMessageAll(JSONUtil.toJsonStr(messageMap));
				}
			} catch (Exception e) {
				System.err.println("关闭连接出错 : " + e.getLocalizedMessage());
			}
		}
	}

	/**
	 * 收到客户端消息后调用的方法
	 *
	 * @param message 客户端发送过来的消息
	 */
	@OnMessage
	public void onMessage(String message) {
		// 刷新心跳时间
		this.heartBeatTime = System.currentTimeMillis();
		// 群发消息
		cn.hutool.json.JSONObject messageJson = JSONUtil.parseObj(message);
		Object type = messageJson.get("type");// 消息类型
		Object toUser = messageJson.get("to");// 接收对象
		// 心跳检测
		if ("999".equals(type)) {
			Map<String, Object> messageMap = new ConcurrentHashMap<>();
			messageMap.put("type", "1");
			messageMap.put("message", "pong");
			messageMap.put("username", "服务器");
			messageMap.put("to", this.username);
			sendMessageOne(JSONUtil.toJsonStr(messageMap), this.username);
			return;
		}
		// 发送消息
		if ("All".equalsIgnoreCase(type + "")) {
			sendMessageAll(message);
		}else {
			sendMessageOne(message, toUser + "");
		}
	}

	/**
	 * 发生错误时调用
	 */
	@OnError
	public void onError(Throwable error) {
		error.printStackTrace();
	}

	/**
	 * 发送消息
	 *
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(String message){
		//this.session.getBasicRemote().sendText(message);//同步
		this.session.getAsyncRemote().sendText(message);// 异步
	}
	
	/**
	 * 发送消息给指定用户
	 *
	 * @param message
	 * @param toUserName
	 */
	public void sendMessageOne(String message, String toUserName) {
		webSocketMap.keySet().forEach(e -> {
			if (e.equals(toUserName)) {
				webSocketMap.get(e).forEach(f -> {
					try {
						f.session.getAsyncRemote().sendText(message);
					} catch (Exception e2) {
						f.session.getAsyncRemote().sendText(message);
					}
				});
			}
		});
	}
}

package com.cqcloud.platform.service;

/**
 * @author weimeilayer@gmail.com
 * @date 💓💕2022年9月8日🐬🐇💓💕
 */
public interface IotMqttService {
	/**
	 * 扩展传输原文
	 * @param message
	 */
	void pushMessageArrived(byte[] message);
}

package com.cqcloud.platform.service.impl;

import org.springframework.stereotype.Service;

import com.cqcloud.platform.service.IotMqttService;

import lombok.AllArgsConstructor;

/**
 * @author weimeilayer@gmail.com
 * @date 💓💕2022年9月8日🐬🐇💓💕
 */
@Service
@AllArgsConstructor
public class IotMqttServiceImpl implements IotMqttService {
	/**
	 * 获取拓展接口原文值
	 * @param message
	 */
	@Override
	public void pushMessageArrived(byte[] message) {
		// 拓展方法
		TcpEventHandler.handleAlarm("设备号","告警信息");
	}
}

package com.cqcloud.platform.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author weimeilayer@gmail.com ✨
 * @date 💓💕 2022年4月12日 🐬🐇 💓💕
 */
@Configuration
public class WebSocketConfig {

	/**
	 * 注入ServerEndpointExporter, 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
	 * @return
	 */
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
进行批量用户的id进行下发webscoket信息

排查结果如下: 1.使用mvn dependency:tree返回如下信息: [INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ zysygh --- [INFO] com.ghzy:zysygh:jar:0.1.196 [INFO] +- org.springframework.boot:spring-boot-starter-data-redis:jar:2.2.13.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.2.13.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot:jar:2.2.13.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.2.13.RELEASE:compile [INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile [INFO] | | \- org.yaml:snakeyaml:jar:1.25:runtime [INFO] | +- org.springframework.data:spring-data-redis:jar:2.2.12.RELEASE:compile [INFO] | | +- org.springframework.data:spring-data-keyvalue:jar:2.2.12.RELEASE:compile [INFO] | | | \- org.springframework.data:spring-data-commons:jar:2.2.12.RELEASE:compile [INFO] | | +- org.springframework:spring-tx:jar:5.2.12.RELEASE:compile [INFO] | | +- org.springframework:spring-oxm:jar:5.2.12.RELEASE:compile [INFO] | | \- org.springframework:spring-aop:jar:5.2.12.RELEASE:compile [INFO] | \- io.lettuce:lettuce-core:jar:5.2.2.RELEASE:compile [INFO] | +- io.netty:netty-common:jar:4.1.58.Final:compile [INFO] | +- io.netty:netty-handler:jar:4.1.58.Final:compile [INFO] | | +- io.netty:netty-resolver:jar:4.1.58.Final:compile [INFO] | | +- io.netty:netty-buffer:jar:4.1.58.Final:compile [INFO] | | \- io.netty:netty-codec:jar:4.1.58.Final:compile [INFO] | +- io.netty:netty-transport:jar:4.1.58.Final:compile [INFO] | \- io.projectreactor:reactor-core:jar:3.3.13.RELEASE:compile [INFO] | \- org.reactivestreams:reactive-streams:jar:1.0.3:compile [INFO] +- org.springframework.boot:spring-boot-starter-mail:jar:2.2.13.RELEASE:compile [INFO] | +- org.springframework:spring-context-support:jar:5.2.12.RELEASE:compile [INFO] | | +- org.springframework:spring-beans:jar:5.2.12.RELEASE:compile [INFO] | | \- org.springframework:spring-context:jar:5.2.12.RELEASE:compile [INFO] | \- com.sun.mail:jakarta.mail:jar:1.6.5:compile [INFO] | \- com.sun.activation:jakarta.activation:jar:1.2.2:compile [INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.2.13.RELEASE:compile [INFO] | +- org.thymeleaf:thymeleaf-spring5:jar:3.0.12.RELEASE:compile [INFO] | | \- org.thymeleaf:thymeleaf:jar:3.0.12.RELEASE:compile [INFO] | | +- org.attoparser:attoparser:jar:2.0.5.RELEASE:compile [INFO] | | \- org.unbescape:unbescape:jar:1.1.6.RELEASE:compile [INFO] | \- org.thymeleaf.extras:thymeleaf-extras-java8time:jar:3.0.4.RELEASE:compile [INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.2.13.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.2.13.RELEASE:compile [INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.10.5:compile [INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.10.5:compile [INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.10.5:compile [INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.2.13.RELEASE:compile [INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.41:compile [INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.41:compile [INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.41:compile [INFO] | +- org.springframework.boot:spring-boot-starter-validation:jar:2.2.13.RELEASE:compile [INFO] | | +- jakarta.validation:jakarta.validation-api:jar:2.0.2:compile [INFO] | | \- org.hibernate.validator:hibernate-validator:jar:6.0.22.Final:compile [INFO] | | +- org.jboss.logging:jboss-logging:jar:3.4.1.Final:compile [INFO] | | \- com.fasterxml:classmate:jar:1.5.1:compile [INFO] | +- org.springframework:spring-web:jar:5.2.12.RELEASE:compile [INFO] | \- org.springframework:spring-webmvc:jar:5.2.12.RELEASE:compile [INFO] | \- org.springframework:spring-expression:jar:5.2.12.RELEASE:compile [INFO] +- org.mybatis.spring.boot:mybatis-spring-boot-starter:jar:2.3.2:compile [INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.2.13.RELEASE:compile [INFO] | | +- com.zaxxer:HikariCP:jar:3.4.5:compile [INFO] | | \- org.springframework:spring-jdbc:jar:5.2.12.RELEASE:compile [INFO] | +- org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure:jar:2.3.2:compile [INFO] | \- org.mybatis:mybatis-spring:jar:2.1.2:compile [INFO] +- org.springframework.boot:spring-boot-starter-logging:jar:2.2.13.RELEASE:compile [INFO] | +- ch.qos.logback:logback-classic:jar:1.2.3:compile [INFO] | | \- ch.qos.logback:logback-core:jar:1.2.3:compile [INFO] | \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile [INFO] +- mysql:mysql-connector-java:jar:8.0.26:runtime [INFO] +- org.projectlombok:lombok:jar:1.18.16:compile (optional) [INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.2.13.RELEASE:test [INFO] | +- org.springframework.boot:spring-boot-test:jar:2.2.13.RELEASE:test [INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.2.13.RELEASE:test [INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test [INFO] | | \- net.minidev:json-smart:jar:2.3:test [INFO] | | \- net.minidev:accessors-smart:jar:1.2:test [INFO] | | \- org.ow2.asm:asm:jar:5.0.4:test [INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:test [INFO] | | \- jakarta.activation:jakarta.activation-api:jar:1.2.2:test [INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.5.2:test [INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.5.2:test [INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:test [INFO] | | | \- org.junit.platform:junit-platform-commons:jar:1.5.2:test [INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.5.2:test [INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.5.2:test [INFO] | +- org.junit.vintage:junit-vintage-engine:jar:5.5.2:test [INFO] | | +- org.apiguardian:apiguardian-api:jar:1.1.0:test [INFO] | | +- org.junit.platform:junit-platform-engine:jar:1.5.2:test [INFO] | | \- junit:junit:jar:4.12:test [INFO] | +- org.mockito:mockito-junit-jupiter:jar:3.1.0:test [INFO] | +- org.assertj:assertj-core:jar:3.13.2:test [INFO] | +- org.hamcrest:hamcrest:jar:2.1:test [INFO] | +- org.mockito:mockito-core:jar:3.1.0:test [INFO] | | +- net.bytebuddy:byte-buddy:jar:1.10.19:test [INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.10.19:test [INFO] | | \- org.objenesis:objenesis:jar:2.6:test [INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test [INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test [INFO] | +- org.springframework:spring-core:jar:5.2.12.RELEASE:compile [INFO] | | \- org.springframework:spring-jcl:jar:5.2.12.RELEASE:compile [INFO] | +- org.springframework:spring-test:jar:5.2.12.RELEASE:test [INFO] | \- org.xmlunit:xmlunit-core:jar:2.6.4:test [INFO] +- com.aliyun:aliyun-java-sdk-core:jar:4.5.3:compile [INFO] | +- com.google.code.gson:gson:jar:2.8.6:compile [INFO] | +- org.apache.httpcomponents:httpclient:jar:4.5.13:compile [INFO] | | \- commons-codec:commons-codec:jar:1.13:compile [INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.14:compile [INFO] | +- commons-logging:commons-logging:jar:1.2:compile [INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile [INFO] | | \- javax.activation:javax.activation-api:jar:1.2.0:compile [INFO] | +- org.jacoco:org.jacoco.agent:jar:runtime:0.8.5:compile [INFO] | +- org.ini4j:ini4j:jar:0.5.4:compile [INFO] | +- org.slf4j:slf4j-api:jar:1.7.30:compile [INFO] | +- io.opentracing:opentracing-api:jar:0.33.0:compile [INFO] | \- io.opentracing:opentracing-util:jar:0.33.0:compile [INFO] | \- io.opentracing:opentracing-noop:jar:0.33.0:compile [INFO] +- com.aliyun:dysmsapi20170525:jar:4.1.0:compile [INFO] | +- com.aliyun:tea-util:jar:0.2.23:compile [INFO] | +- com.aliyun:endpoint-util:jar:0.0.7:compile [INFO] | +- com.aliyun:tea:jar:1.3.1:compile [INFO] | | \- com.squareup.okhttp3:okhttp:jar:3.14.9:compile [INFO] | | \- com.squareup.okio:okio:jar:1.17.2:compile [INFO] | +- com.aliyun:tea-openapi:jar:0.3.8:compile [INFO] | | +- com.aliyun:credentials-java:jar:1.0.1:compile [INFO] | | | \- com.aliyun:credentials-api:jar:1.0.0:compile [INFO] | | +- com.aliyun:alibabacloud-gateway-spi:jar:0.0.2:compile [INFO] | | \- com.aliyun:tea-xml:jar:0.1.6:compile [INFO] | | \- org.dom4j:dom4j:jar:2.0.3:compile [INFO] | \- com.aliyun:openapiutil:jar:0.2.2:compile [INFO] | \- org.bouncycastle:bcprov-jdk18on:jar:1.78.1:compile [INFO] +- com.alibaba:fastjson:jar:1.2.75:compile [INFO] +- org.mybatis:mybatis:jar:3.5.9:compile [INFO] +- org.jetbrains:annotations:jar:13.0:compile [INFO] +- org.springframework.boot:spring-boot-configuration-processor:jar:2.2.13.RELEASE:compile (optional) [INFO] +- redis.clients:jedis:jar:3.1.0:compile [INFO] | \- org.apache.commons:commons-pool2:jar:2.7.0:compile [INFO] +- io.jsonwebtoken:jjwt-api:jar:0.11.5:compile [INFO] +- io.jsonwebtoken:jjwt-impl:jar:0.11.5:runtime [INFO] \- io.jsonwebtoken:jjwt-jackson:jar:0.11.5:runtime [INFO] \- com.fasterxml.jackson.core:jackson-databind:jar:2.10.5.1:compile [INFO] +- com.fasterxml.jackson.core:jackson-annotations:jar:2.10.5:compile [INFO] \- com.fasterxml.jackson.core:jackson-core:jar:2.10.5:compile [INFO] ------------------------------------------------------------------------ 2.applicaion.properties中的配置内容为logging.config=classpath:logback-spring.xml 3.src/main/resources目录下存在logback-spring.xml文件,但之前里面没有那两行<include>配置项,现已添加。
最新发布
07-19
<think>我们之前已经排除了log4j的依赖,并且确认了依赖树中没有log4j,但是问题依然存在。 现在,根据新的排查结果: 1. 依赖树中确实没有log4j相关的依赖,只有logback和slf4j。 2. 在application.properties中配置了`logging.config=classpath:logback-spring.xml`,指向Logback的配置。 3. 在src/main/resources下已经存在logback-spring.xml,并且已经添加了<include>配置。 但是,错误信息仍然显示在尝试初始化日志系统时,它去查找`log4j-spring.xml`,而不是使用Logback。 这可能是由于Spring Boot的日志初始化顺序问题。在Spring Boot启动的早期,日志系统需要被初始化,而在这个阶段,`application.properties`中的配置可能还没有被加载。因此,我们设置的`logging.config`可能没有在日志系统初始化之前生效。 解决方案: 1. 使用`spring.xml`后缀的配置文件:Spring Boot对`logback-spring.xml`有特殊处理,它会在Spring环境完全初始化后才加载,这样可以使用Spring的属性配置。但是,在Spring环境初始化之前,日志系统需要先初始化,所以这个文件可能没有被及时使用。 2. 尝试使用默认的日志配置文件名称:将配置文件命名为`logback.xml`(不带`-spring`),并移除`application.properties`中的`logging.config`配置。这样,Spring Boot会在classpath中查找`logback.xml`并在启动早期初始化日志系统。 3. 或者,将配置放在`logback.xml`中,并在`logback.xml`中通过`<springProperty>`标签引入Spring属性(如果需要)。 4. 确保没有其他地方的配置覆盖了日志配置。 步骤: a. 将`src/main/resources/logback-spring.xml`重命名为`logback.xml`。 b. 删除`application.properties`中的`logging.config=classpath:logback-spring.xml`这一行(避免重复指定)。 c. 清理并重新打包项目,然后部署。 为什么这样做? 因为Spring Boot在启动时,会按以下顺序查找日志配置: - 首先,检查系统属性`logging.config`(这通常由`application.properties`中的`logging.config`设置,但在加载属性文件之前,日志系统需要初始化,所以这会导致循环问题)。 - 然后,在classpath中查找`logback-spring.xml`(但需要Spring环境,所以初始化较晚)。 - 而`logback.xml`(不带`-spring`)会被Logback自动加载,且不需要Spring环境,因此可以在更早的阶段初始化。 因此,使用`logback.xml`可以让日志系统在Spring Boot启动的最初阶段就正确初始化,而不会去尝试加载log4j的配置。 如果问题仍然存在,可以在`logback.xml`中增加一个简单的配置,确保它被正确加载,例如: ```xml <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> </configuration> ``` 另外,检查一下是否在代码中通过系统属性设置了`logging.config`,例如在`main`方法中: ```java public static void main(String[] args) { System.setProperty("logging.config", "classpath:log4j-spring.xml"); // 确保没有这样的代码 SpringApplication.run(Application.class, args); } ``` 如果以上步骤都做了还是不行,可以考虑在启动命令中增加参数来指定日志配置(但一般不推荐,因为问题应该从项目内部解决): ```bash java -jar your-app.jar --logging.config=classpath:logback.xml ``` 但是,根据我们的调整,应该不需要这样做。 最后,如果以上方法都不能解决问题,那么可能是由于某些依赖库在内部强制设置了日志系统(例如使用了Log4j的桥接器),但根据依赖树,我们没有看到log4j的依赖,所以这种可能性较小。 让我们重新整理一下步骤: 1. 重命名配置文件:将`logback-spring.xml`改为`logback.xml`。 2. 移除`application.properties`中的`logging.config`配置项。 3. 确保`logback.xml`中有基本的配置(至少包含一个控制台输出)。 4. 清理项目并重新打包。 5. 检查打包后的jar文件中,`logback.xml`是否在根目录下(即`BOOT-INF/classes/logback.xml`)。 6. 运行jar文件,观察日志。 如果问题解决,那么就是日志初始化顺序的问题。如果仍然出现同样的错误,那么我们需要进一步排查。 另外,注意一个细节:在错误堆栈中,我们看到的是: `org.springframework.boot.logging.logback.LogbackLoggingSystem.loadConfiguration` 它试图加载`log4j-spring.xml`?这很奇怪,因为`LogbackLoggingSystem`是Logback的日志系统,它不应该去加载log4j的配置。 因此,另一种可能是:在应用启动过程中,有多个日志系统被尝试初始化。Spring Boot会尝试加载多个日志系统(按Logback、Log4j2、Java Util Logging的顺序),直到找到一个可用的。但是,在依赖中我们已经排除了log4j,所以它应该不会去尝试Log4j。 然而,错误信息中显示的是`Logging system failed to initialize using configuration from &#39;classpath:log4j-spring.xml&#39;`,这可能是由于在初始化Logback时发生了错误,然后回退到Log4j?但是根据依赖,我们并没有Log4j,所以这个尝试会失败。 所以,我们还需要检查`logback.xml`的配置是否正确,避免Logback初始化失败。 因此,在修改配置后,请确保Logback能正确初始化。可以在`logback.xml`中增加一个调试信息,或者使用最简单的配置。 总结:先尝试重命名和移除配置项,确保Logback在早期正确初始化。 如果问题依旧,请提供新的错误日志。</think>### 问题分析与解决方案 根据您提供的依赖树和配置信息,问题核心在于**日志系统初始化顺序冲突**。虽然已排除Log4j依赖并配置Logback,但Spring Boot仍在尝试加载Log4j配置。以下是具体原因和解决方案: #### 根本原因: 1. **依赖冲突**:`com.aliyun:aliyun-java-sdk-core` 引入了 `javax.xml.bind:jaxb-api` 和 `org.slf4j:slf4j-api`,但未明确排除旧版日志桥接器 2. **初始化顺序**:Spring Boot在加载`application.properties`之前初始化日志系统 3. **配置残留**:可能存在环境变量或系统属性强制指定Log4j --- ### 解决方案 #### 1. 强制指定日志系统(关键步骤) 在 `src/main/resources` 下创建 `spring.properties` 文件: ```properties # 强制使用Logback,禁止自动检测其他日志框架 logging.system=logback ``` #### 2. 修复依赖冲突 在 `pom.xml` 中添加显式排除: ```xml <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <exclusions> <!-- 排除旧版SLF4J实现 --> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <!-- 排除可能引起冲突的XML库 --> <exclusion> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </exclusion> </exclusions> </dependency> ``` #### 3. 增强Logback配置 更新 `logback-spring.xml` 确保完全自包含: ```xml <?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="30 seconds"> <!-- 禁用服务发现,避免加载外部配置 --> <contextName>local</contextName> <!-- 控制台输出 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 根日志级别 --> <root level="INFO"> <appender-ref ref="CONSOLE" /> </root> <!-- 重要:禁用Spring Boot的默认配置加载 --> <include resource="org/springframework/boot/logging/logback/base.xml" optional="true"/> </configuration> ``` #### 4. 添加日志初始化监听器 在 `src/main/resources/META-INF` 下创建 `spring.factories` 文件: ```properties # 提前初始化日志系统 org.springframework.context.ApplicationListener=\ org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.logging.LoggingApplicationListener ``` #### 5. 验证打包结果 在打包后执行以下检查: ```bash # 检查日志配置是否包含 unzip -l target/zysygh.jar | grep -E &#39;logback-spring.xml|spring.factories|spring.properties&#39; # 检查依赖冲突 mvn dependency:tree -Dincludes=org.slf4j,ch.qos.logback ``` --- ### 原理解释 Spring Boot日志初始化流程存在以下敏感点: ```mermaid graph LR A[启动JVM] --> B[读取spring.factories] B --> C[LoggingApplicationListener] C --> D{检测日志系统类型} D -->|系统属性| E[使用指定日志框架] D -->|自动检测| F[扫描classpath] F --> G[找到logback-classic] G --> H[加载logback-spring.xml] F --> I[发现slf4j-log4j痕迹] --> J[尝试加载log4j配置] ``` 您遇到的错误发生在 **步骤J**,因为: 1. Aliyun SDK引入了`slf4j-api`但未提供实现 2. 类路径中存在`org.slf4j.impl.StaticLoggerBinder`的旧版本 3. Spring误判需要加载Log4j --- ### 部署验证 在宝塔面板重启应用后,检查日志开头应出现: ``` 2023-08-01 12:00:00.000 [main] INFO o.s.b.StartupInfoLogger - Starting ZysyghApplication on bt-server with PID 12345 ``` 如果仍有问题,添加JVM参数: ```bash -Dorg.springframework.boot.logging.LoggingSystem=none ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

❀͜͡傀儡师

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值