ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Socket + Swing + Oracle:银行排号系统全链路开发实战

Java Socket + Swing + Oracle:银行排号系统全链路开发实战 简介这套基于Java Socket与Java GUI的银行排号系统项目面向计算机专业学生、课程设计/毕业设计开发者以及希望掌握CS架构通信的Java学习者。系统以Socket实现客户端与服务端的网络通信以Java GUI构建操作界面围绕银行排号业务展示从取号、排队到窗口呼叫的完整流程能够帮助读者将Java网络编程、图形界面设计与数据库应用串联起来。压缩包内包含项目全部源码与配套文档源码经过测试校正可直接运行整体大小约292.61MB主要涉及Java源代码文件、工程配置信息及说明文档目录结构便于查找和复用。目前已有475人浏览学习可作同类项目的设计参考。通过该资源读者能获得可运行的排号系统源码、设计文档以及Oracle数据库使用思路既可作为模板进行二次开发也能用于深入理解Socket编程和多线程在真实业务场景中的配合方式。1. 银行排号系统到底在练什么一次完整的 Java C/S 全链路在银行大厅里取号机、柜台窗口、叫号大屏这三样东西是异步工作的用户按键取号柜员叫下一个号大屏把当前队列滚动出来。背后需要一台服务器同时协调通信、维护队列状态、并把每一笔业务落到数据库里。很多 Java 学习者拿到「基于 Java Socket Java GUI 的银行排号系统」这个题目时第一反应是去网上找一个源码包解压然后本地把 Oracle 一装、界面一跑就以为完成了。真到了答辩或演示阶段被问「你的通信协议怎么设计的并发取号会不会重复Oracle 挂了界面能提示吗」多半会沉默。这个标题看起来是三个技术的堆叠——Socket 网络编程、Swing 界面、Oracle 存储——其实它练的是把三者串成一条完整链路的工程能力。它非常适合课程设计、毕业设计或者当作 Java 基础阶段的综合练习项目规模不大但每一步都会遇到真实的网络和数据库问题这正是从写练习题代码过渡到写业务系统之间最缺的一环。2. 整体架构这么定后面才不用返工模块拆分与关键技术选型2.1 C/S 三层模型GUI、通信、持久化各自管什么标题里的「Java GUI Socket Oracle」可以映射成三个独立层次。表示层是 Java Swing 客户端包含取号机、柜台、大屏三类界面通信层是原生 java.net.Socket/ServerSocket负责消息的可靠收发持久层是 Oracle 数据库记录票号流水、窗口状态、办结日志。我在做这类排号系统时会把通信层抽成一个独立的 common 包客户端和服务端共同依赖它而不是把报文解析代码散落在界面监听器和 ServerSocket 循环里。排号业务的状态变化是双向的取号机要发出「取号请求」柜台要发出「叫号请求」大屏要收到「队列变化推送」这三类消息会流向不同连接。如果通信代码散落后面排查粘包、乱码、断线重连时会非常痛苦。模块拆分通常是这样的server 包Server、ClientSession、Dispatcher、Broadcaster、QueueStoreclient 包TakeTicketPanel、WindowPanel、DisplayPanel、SocketClientcommon 包Frame、FrameCodec、MsgType、QueueSnap分层里最容易让人忽略的是客户端并不直接访问 Oracle。取号、叫号全部走 Socket 到服务端由服务端统一访问 Oracle。常见翻车是有人把 JDBC 也写进客户端导致每台取号机都要装 Oracle 客户端、每个界面都要配数据源。正确做法是客户端与服务端保持一个轻量 TCP 长连接数据库只存在于服务端一侧客户端连 Oracle 的凭据永远不应该出现在 GUI 进程里。2.2 为什么不换 Netty 或 JavaFX原生 Socket 和 Swing 的取舍做这个题目经常会被建议换 Netty JavaFX Spring Boot。我自己的判断是对于一个课设或毕设不建议为了「技术显摆」就换掉标题里的原始技术栈。标题明确写了 Socket通常意味着考核点就是原生 TCP 编程。你换了 Netty核心代码变成框架回调答辩时很难展示对 Socket 本身的理解反而容易被追问「Netty 的 eventLoop 怎么工作的」这类更深入的问题。排号系统的并发量非常低一个网点 20 个柜台、3 台取号机、2 块大屏撑死几十个并发连接。阻塞 IO 模型完全扛得住没必要引入异步框架增加排错难度。Swing 是 Java 标准库自带 GUI无需额外依赖。JavaFX 虽然界面更现代但打包体积、学习成本、和大多数课程设计要求的 JDK 环境都不太匹配。我一般会给一个判定标准如果目标只是把系统跑通并解释清楚就用原生 Socket Swing 最稳如果你明确想拿差异化分数先把原生版做好再在文档里写一个 Netty 演进方案。这样既能守住课设的地基又能展示你对框架迁移有思考。2.3 并发模型连接线程、业务线程、数据库连接的边界排号系统的并发核心不在数据库而在服务端内存里的共享队列状态。我把当前排队队列维护在服务端客户端角色都通过消息来读写它。一个简单可用的内存队列结构长这样// QueueStore.java —— 服务端内存队列三个角色读写同一份快照 public class QueueStore { private final MapInteger, Ticket queue new ConcurrentHashMap(); private final AtomicInteger ticketSeq new AtomicInteger(0); // 取号本地序号递增具体票号字符串交给 Oracle 生成 public Ticket takeTicket(String type) { int seq ticketSeq.incrementAndGet(); Ticket t new Ticket(seq, type, WAIT, null); queue.put(seq, t); return t; } // 叫号按排队顺序找到第一个 WAIT 状态改成 CALLED 并绑定窗口 public void callNext(int windowNo) { queue.values().stream() .filter(t - WAIT.equals(t.status)) .findFirst() .ifPresent(t - { t.status CALLED; t.windowNo windowNo; }); } }这里用 AtomicInteger 保证取号本地序号不重复ConcurrentHashMap 保证多线程下读写不抛 ConcurrentModificationException。注意本地 seq 和最终打印出来的票号是两回事真正的票号字符串比如 A20250112001由 Oracle 存储过程生成本地 seq 只用于快速排序和状态流转。跨营业日清零的问题会在第 5 章专门处理。服务端的并发边界就是三条accept 线程只负责收连接不碰业务每个 Socket 连接交给线程池处理数据库连接单独走连接池绝不在 Socket IO 线程里直接执行 JDBC。把这几个边界画清楚后面写代码就不会乱。3. Socket 网络编程的核心协议设计、线程安全与报文解析3.1 报文格式先定死长度包头 JSON 负载粘包半包就没了Socket 网络编程里第一个被追问的问题就是 TCP 粘包。排号系统的消息类型并不算多但每一条都是独立业务动作取号、叫号、队列快照、心跳、错误提示。如果每次都直接把字符串 write 进流对端 read 时经常会一次读到多条消息或者只读到半个消息。以前我调试这类问题全靠玄学直到把帧格式固定下来。我惯用的格式是「4 字节长度头 1 字节类型 UTF-8 JSON」// FrameCodec.java —— 定长包头 JSON 负载的编解码 public class FrameCodec { private static final int HEADER_LEN 5; // 4 字节长度 1 字节类型 private static final int MAX_FRAME 64 * 1024; // 单帧限制 64KB防恶意超大包 public static Frame read(InputStream in) throws IOException { DataInputStream dis new DataInputStream(in); byte[] header new byte[HEADER_LEN]; dis.readFully(header); // 读不足 5 字节会抛 EOFException不会返回半包 int bodyLen ((header[0] 0xFF) 24) | ((header[1] 0xFF) 16) | ((header[2] 0xFF) 8) | (header[3] 0xFF); if (bodyLen 0 || bodyLen MAX_FRAME) { throw new IOException(非法帧长度: bodyLen); } byte type header[4]; byte[] body new byte[bodyLen]; dis.readFully(body); // 只读 bodyLen 字节剩余字节留给下一帧 return new Frame(type, body); } public static void write(OutputStream out, byte type, String json) throws IOException { byte[] body json.getBytes(StandardCharsets.UTF_8); ByteBuffer buf ByteBuffer.allocate(HEADER_LEN body.length); buf.putInt(body.length); buf.put(type); buf.put(body); out.write(buf.array()); out.flush(); } }这段代码的逻辑是先读满 5 字节头部解析出 body 长度和消息类型再根据长度读 body。由于 readFully 保证「读不够就继续阻塞直到读满或连接断开」所以一个循环里反复调用 read 就能稳定切分消息流不会出现读到半个 JSON 的情况。参数上我限制单帧最大 64KB排号系统最多几千号、几十个窗口一个队列快照不会超过这个量级限制它主要是防止某个异常客户端发送超大长度头导致服务端申请大内存被拖死。提示如果只是做课设不要求跨语言也可以直接用 Java 的 DataOutputStream.writeUTF 配合 readUTF但手工帧对后续抓包排障、对接其他语言客户端都更透明建议按上面的方式做。3.2 服务端连接管理线程池、会话上下文与消息分发服务端骨架的常见做法是主线程只做 accept每个新连接包装成一个 ClientSession 对象注册进全局会话表再把后续处理丢给线程池。ClientSession 里至少要有 Socket、输入流、输出流、会话 id、最近心跳时间ClientRegistry 是全局注册表第 3.4 节的广播要遍历它。// Server.java —— 主线程只负责 accept连接处理全部丢给线程池 public class Server { private final ThreadPoolExecutor pool new ThreadPoolExecutor( 8, 16, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(256), r - new Thread(r, conn-handler), // 线程工厂方便 jstack 定位 new ThreadPoolExecutor.CallerRunsPolicy()); public void start(int port) throws IOException { try (ServerSocket ss new ServerSocket()) { ss.setReuseAddress(true); ss.bind(new InetSocketAddress(port)); System.out.println(server on port port); while (true) { Socket socket ss.accept(); try { socket.setSoTimeout(90_000); // 90 秒无数据视为死连接 ClientSession session new ClientSession(socket); ClientRegistry.INSTANCE.add(session); pool.execute(() - handleSession(session)); } catch (RejectedExecutionException e) { socket.close(); // 队列满直接拒别硬塞 } } } } private void handleSession(ClientSession session) { try { while (true) { Frame frame FrameCodec.read(session.getInputStream()); Dispatcher.dispatch(session, frame); // 按 type 路由到业务处理 } } catch (IOException e) { ClientRegistry.INSTANCE.remove(session); } } }线程池参数这么设是有意义的核心 8、最大 16 线程已经覆盖「20 个柜台 3 台取号机 2 块大屏」的连接峰值因为同一时刻真正在收发消息的线程远少于连接数。队列容量 256 是为了应对重连风暴比如服务端重启瞬间所有客户端同时重连256 个排队任务足够缓冲。CallerRunsPolicy 的意思是队列满消息时会让 accept 主线程代跑任务宁可 accept 暂时慢一点也不能静默丢弃连接。服务端 read 循环里每次 readFully 都是阻塞的如果客户端异常断开readFully 会抛 EOFException外层 catch 负责把会话从注册表里清掉。3.3 心跳保活与断线重连让排号机掉线了也能自动恢复TCP 长连接最典型的问题不是「断开」而是「半开」。比如网点交换机断电、网线被误拔客户端 Socket 可能还认为连接正常取号按钮一直转圈。心跳的作用就是定期探测对端是否还活着。客户端每 30 秒发一个心跳帧服务端 90 秒没收到数据就关闭连接。这个「90 秒 3 次心跳」的比例是为了容忍一次网络抖动。// SocketClient.java —— 带心跳和指数退避重连的客户端 public class SocketClient { private volatile Socket socket; private volatile boolean running true; private final String host; private final int port; public SocketClient(String host, int port) { this.host host; this.port port; } public void start() { new Thread(() - { int retrySec 1; while (running) { try { socket new Socket(); socket.connect(new InetSocketAddress(host, port), 3000); retrySec 1; // 连接成功后重置退避间隔 heartbeatLoop(socket); // 阻塞直到连接断开 } catch (IOException e) { retrySec Math.min(retrySec 1, 30); try { Thread.sleep(TimeUnit.SECONDS.toMillis(retrySec)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); return; } } } }, connect-main).start(); } private void heartbeatLoop(Socket s) { try { while (running !s.isClosed()) { Thread.sleep(30_000); FrameCodec.write(s.getOutputStream(), MsgType.HEARTBEAT, {\heartbeat\:true}); } } catch (Exception e) { // 任何异常都跳出外层循环进入重连分支 } } }connect 超时设 3000ms 很关键如果服务器没启动客户端界面要在 3 秒内给出失败提示而不是让用户等操作系统默认的几十秒超时。重连退避从 1 秒开始翻倍封顶 30 秒。这样服务器重启后所有客户端会在半分钟内自动恢复连接不需要人工点「重连」按钮——这是演示时非常加分的细节。3.4 号牌广播窗口叫号后怎么让等待屏和取号机同时看到排号系统里所有状态变化最终都要让三个角色看到最自然的方式是服务端主动广播而不是让大屏轮询。广播逻辑集中在一个类里// Broadcaster.java —— 向所有客户端广播最新队列快照 public class Broadcaster { private final SetClientSession sessions new CopyOnWriteArraySet(); public void add(ClientSession s) { sessions.add(s); } public void remove(ClientSession s) { sessions.remove(s); } public void broadcastTo(byte msgType, String json) { for (ClientSession s : sessions) { try { FrameCodec.write(s.getOutputStream(), msgType, json); } catch (IOException e) { sessions.remove(s); // 写失败说明连接已死顺手摘掉 } } } }用 CopyOnWriteArraySet 而不是普通 ArrayList是因为广播在遍历 sessions 的同时新的取号机可能正在加入、死连接可能正在移除。这个集合读多写少遍历时复制一份快照不会抛 ConcurrentModificationException。广播的时机要放在业务落库之后先 UPDATE Oracle 把票号状态改成 CALLED再广播队列快照。如果先广播后落库数据库回滚时大屏已经显示叫号数据就对不上了。大屏客户端在连接建立时通过注册消息带上 roledisplay服务端可以把它单独分进「display 广播组」。这样窗口客户端收到队列快照后要刷新自己的服务号大屏收到快照后要渲染滚动队列二者看到的同一份数据、UI 表现各自独立。4. Java GUI 客户端的 Swing 落地取号、叫号、大屏三端联动4.1 客户端的三个角色取号机、柜台、大屏在界面上怎么分工Swing 的 GUI 代码最忌讳的是把三个角色的界面塞进同一个 JFrame演示时来回切换窗口既乱又容易让人误以为逻辑纠缠在一起。我习惯用启动参数指定角色同一个客户端 jar 通过 --role 参数决定加载哪个面板take取号机一个大按钮 当前票号标签适合触摸屏。window柜台显示当前服务号 「呼叫下一个」按钮 「办结」按钮。display大屏JTable 只读显示队列当前叫号行高亮。// Main.java —— 按 --role 参数组装不同界面 public static void main(String[] args) throws Exception { String role getArgValue(args, --role, take); // take/window/display SocketClient client new SocketClient(127.0.0.1, 9000); client.start(); // 心跳线程先启动 EventQueue.invokeLater(() - { JFrame frame new JFrame(银行排号系统- role); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); if (window.equals(role)) { frame.add(new WindowPanel(client)); } else if (display.equals(role)) { frame.add(new DisplayPanel(client)); } else { frame.add(new TakeTicketPanel(client)); } frame.pack(); frame.setVisible(true); }); }EventQueue.invokeLater 的作用是把界面创建放到 EDTEvent Dispatch Thread上执行这是 Swing 的单线程模型要求。SocketClient 的网络线程和 EDT 从此分开网络线程负责收发EDT 负责画界面。三个面板的职责也都单一定位取号面板不做叫号逻辑柜台面板不做队列渲染。4.2 Swing 事件线程与 Socket 读写分离不卡界面的取号按钮Swing 的事件回调跑在 EDT。如果在按钮监听器里直接 new Socket 并等待服务器响应UI 会冻结——按钮点下去界面白屏直到服务器返回或超时。这在演示时是硬伤因为服务器一慢评审会认为整个系统死了。正确做法是按钮点击后用 SwingWorker 把网络操作移到后台线程// TakeTicketPanel.java —— 取号按钮的事件与后台线程分离 public class TakeTicketPanel extends JPanel { private final SocketClient client; private final JLabel currentLabel new JLabel(请点击取号, SwingConstants.CENTER); private final JButton takeBtn new JButton(取号); public TakeTicketPanel(SocketClient client) { this.client client; takeBtn.addActionListener(e - { takeBtn.setEnabled(false); // 防连点一次点击只能对应一个号 new SwingWorkerString, Void() { Override protected String doInBackground() throws Exception { return client.requestTicket(); // 发 TAKE_TICKET 并等响应 } Override protected void done() { try { currentLabel.setText(您已取号 get()); } catch (Exception ex) { currentLabel.setText(取号失败 ex.getMessage()); } finally { takeBtn.setEnabled(true); } } }.execute(); }); } }按钮点击后立刻禁用防止用户狂点产生多个号码doInBackground 跑在 worker 线程不阻塞 EDTdone() 方法由 SwingWorker 在 EDT 上回调所以里面可以直接改 JLabel。如果服务器没有启动客户端会在 3 秒内收到连接超时异常界面上看到「取号失败connect timed out」而不是永久卡死。窗口柜台的「呼叫下一个」按钮逻辑完全一样只是把 requestTicket 换成 callNext返回的数据是「下一个被叫到的票号」。柜台的按钮点击后也应该禁用等服务端确认叫号成功再恢复否则快速双击会连续叫出两个号。4.3 客户端消息分发把服务器推送翻译成界面动作客户端收到的消息包括服务器主动推送的队列快照、叫号结果、错误提示。网络读取线程拿到 Frame 后不应该直接碰 Swing 组件而是通过一个分发器转给对应面板的回调方法// ClientDispatcher.java —— 客户端侧消息分发 public class ClientDispatcher { private final MapByte, ConsumerFrame handlers new HashMap(); public ClientDispatcher(DisplayPanel display, WindowPanel window, TakeTicketPanel take) { handlers.put(MsgType.QUEUE_SNAPSHOT, f - display.refresh(f)); handlers.put(MsgType.CALL_NEXT, f - window.showCalled(f)); handlers.put(MsgType.TAKE_TICKET, f - take.showTicket(f)); handlers.put(MsgType.ERROR, f - showError(f)); } public void dispatch(Frame frame) { ConsumerFrame fn handlers.get(frame.getType()); if (fn ! null) { fn.accept(frame); } } }用 Map 而不是 switch 的好处是新增消息类型时不用改动网络读取循环。注意这里的 Consumer 回调是在网络读取线程里执行的所以 display.refresh、window.showCalled 这些方法内部必须再用 SwingUtilities.invokeLater 包一层把真正的 UI 更新切回 EDT。这个细节如果不做界面会出现偶发的不刷新或者组件状态异常属于 Swing 开发里最典型的线程违规问题。5. Oracle 持久化与高频故障排查序列、存储过程和四个必踩坑5.1 表结构设计排号流水、窗口状态与工单记录数据库这边最少要三张表窗口表、票号流水表、当日序号表。窗口表记录每个窗口的状态和当前服务票号票号流水表记录每一笔业务从取号到办结的完整生命周期当日序号表单独存放当天发放了多少号这是存储过程里做原子递增的基础。-- 排号业务三张核心表Oracle 11g CREATE TABLE t_window ( window_no NUMBER(2) NOT NULL, -- 窗口号01~20 window_name VARCHAR2(30) NOT NULL, -- 窗口显示名 status NUMBER(1) DEFAULT 0, -- 0 空闲 1 忙碌 2 暂停 current_no VARCHAR2(12), -- 当前正在服务的票号 CONSTRAINT pk_window PRIMARY KEY (window_no) ); CREATE TABLE t_ticket_record ( ticket_no VARCHAR2(12) NOT NULL, -- 票号类型日期当日流水 ticket_type CHAR(1) DEFAULT A, -- A 个人 B 对公 C 理财 window_no NUMBER(2), -- 服务窗口NULL 表示还没被叫 status NUMBER(1) DEFAULT 0, -- 0 等待 1 呼叫 2 结束 3 过号 create_time DATE DEFAULT SYSDATE, call_time DATE, finish_time DATE, CONSTRAINT pk_ticket PRIMARY KEY (ticket_no) ); CREATE TABLE t_day_seq ( biz_date DATE NOT NULL, -- 营业日精确到天 seq NUMBER DEFAULT 0, -- 当日已发放流水 CONSTRAINT pk_day_seq PRIMARY KEY (biz_date) );票号用 VARCHAR2 而不是拼接 NUMBER是因为最终展示的字符串「A20250112001」包含了类型、日期、流水三段信息直接存字符串便于窗口和大屏按原样显示。t_day_seq 单独拆出来是为了让票号生成操作定位到这一行做行级更新这是第 5.2 节存储过程的关键。5.2 票号生成用 Oracle SEQUENCE 还是存储过程有些课设版本直接用 Oracle SEQUENCE 的 NEXTVAL 生成票号。这样很简单但有两个硬伤第一NEXTVAL 是全局递增的跨天不会自动清零第二天票号直接从昨天的尾号继续走第二票号里要带日期和类型前缀SEQUENCE 做不到「按日 按类型」分段。常见做法是把票号生成写成存储过程服务端取号时调用它-- 生成票号按营业日原子递增避免并发取到同一号 CREATE OR REPLACE PROCEDURE sp_take_ticket( p_type IN CHAR, p_ticket_no OUT VARCHAR2 ) AS v_seq NUMBER; v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM t_day_seq WHERE biz_date TRUNC(SYSDATE); IF v_count 0 THEN INSERT INTO t_day_seq(biz_date, seq) VALUES (TRUNC(SYSDATE), 0); END IF; -- 关键UPDATE ... RETURNING 在同一行上加行级锁天然串行 UPDATE t_day_seq SET seq seq 1 WHERE biz_date TRUNC(SYSDATE) RETURNING seq INTO v_seq; p_ticket_no : p_type || TO_CHAR(SYSDATE, YYYYMMDD) || LPAD(v_seq, 4, 0); INSERT INTO t_ticket_record(ticket_no, ticket_type) VALUES (p_ticket_no, p_type); COMMIT; END;UPDATE ... RETURNING 是这个存储过程的核心多个客户端同时取号时UPDATE 会在这行 t_day_seq 上加行级锁后面的会话只能等前一个 COMMIT 后才执行因此并发下不可能取到重复流水号。LPAD(v_seq, 4, 0) 保证当天第 1 号显示为 0001 而不是 1票号长度统一、排序好看。注意跨营业日零点的瞬间t_day_seq 的行锁竞争会造成短暂阻塞但排号系统一天最多几千号这个竞争可以接受。不要为了「优化」引入更复杂的锁表逻辑反而增加排查难度。5.3 JDBC 提交与事务边界别把每次叫号都做成跨网络事务JDBC 这块最常见的误区是每次操作都手动 setAutoCommit(false)然后忘记 COMMIT或者每次都新建连接。排号系统的事务边界很清晰取号一次调用存储过程过程内提交叫号一次 UPDATE立刻提交办结一次 UPDATE立刻提交读队列用 SELECT不需要事务。叫号的 UPDATE 必须带状态条件防止两个窗口抢同一个号// TicketDao.callNext —— 叫号动作的 JDBC 写法 public boolean callNext(String ticketNo, int windowNo, Connection conn) { String sql UPDATE t_ticket_record SET window_no?, status1, call_timeSYSDATE WHERE ticket_no? AND status0; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, windowNo); ps.setString(2, ticketNo); int rows ps.executeUpdate(); conn.commit(); return rows 1; // rows0 说明票已被别的窗口叫走 } catch (SQLException e) { conn.rollback(); throw new RuntimeException(叫号失败, e); } }WHERE status0 这个条件的作用是同一张票被两个窗口同时抢数据库行锁会让第二个 UPDATE 等到第一个提交后执行然后 WHERE 匹配 0 行返回 false。业务层收到 false 就提示「该号已被呼叫」不会出现两个窗口同时服务一个号。每次调用的 Connection 从连接池拿用完归还绝不在 Socket 线程里手动管理连接生命周期。另外你可能见过用SELECT 1 FROM DUAL测试数据库连通性这没问题。DUAL 只是 Oracle 提供的单行单列表用来执行不带表名的表达式查询不要在它上面做业务存储更不用纠结它「最多能存多大」——它本来就不是给业务数据用的。5.4 高频坑排查监听起不来、端口被占、字符集乱码、驱动找不到Oracle 监听服务无法启动 / ORA-12541: TNS:no listener。现象是客户端连接时报 no listener服务端 lsnrctl status 显示没有监听。原因是 Windows 服务里 OracleOraDb11g_home1TNSListener 没启动或者 1521 端口被别的程序占用或者 hosts 文件里 127.0.0.1 不生效。解决步骤是先用lsnrctl status看监听状态再用netstat -ano | findstr 1521查端口占用最后检查 listener.ora 里的 HOST 配置。公司电脑经常同时装多个 Oracle 版本导致监听名冲突把相关监听服务全部停掉再启动目标版本的服务即可。Socket 绑定端口报「通常每个套接字地址协议/网络地址/端口只允许使用一次」。现象是服务端第二次启动时 bind 失败。原因是上一次进程没退出或者 TCP TIME_WAIT 状态导致端口短时间内被内核占用。解决方法是 ServerSocket 先 setReuseAddress(true)close 统一放 finally调试时用 jps 查残留 java 进程确认没有多个 server 实例互抢端口。中文乱码界面按钮正常服务端返回的中文变成 ?。现象是票号类型「对公业务」在窗口端显示成问号。原因是 Socket 流两端编码不一致Windows 下输入输出流默认走 GBK。解决方法是统一在 FrameCodec 里用 UTF-8 读写客户端读取时显式 new String(body, StandardCharsets.UTF_8)不要依赖平台默认字符集。还要检查 Oracle 的 NLS_CHARACTERSET 是否为 AL32UTF8如果是 AL16UTF16VARCHAR2 字段存储中文会翻倍占用可能触发 ORA-12899 字段超长。JDBC 驱动找不到或版本不匹配。现象是 Class.forName 报 ClassNotFoundException: oracle.jdbc.OracleDriver或者 ojdbc6 在 JDK 17 上加载失败。原因是驱动 jar 没打进 classpath或者驱动版本老于 JDK 版本模块化后没有自动解包访问。解决方法是 Oracle 11g 环境用 ojdbc8 或 ojdbc11不要再用 11.2 时代的 ojdbc6JDK 17 下如果还报模块访问异常加 JVM 参数--add-opensjava.base/java.nioALL-UNNAMED。如果只是做课设且环境可控最简单的是直接换 JDK 8 ojdbc8 组合。6. 验证与进阶如何让这套系统经得起演示和追问演示前先做一次「并发取号验证」最有价值同时开 50 个客户端线程去调 sp_take_ticket看生成的 ticket_no 有没有重复。排号系统业务简单最容易出问题的就是这个序号生成。下面是演示前必过的验证集这些点每一个都能对应到面试里常被追问的并发、网络、事务细节。场景操作预期结果取号取号机点按钮票号带日期前缀且递增界面显示同一号码叫号柜台点「呼叫下一个」大屏当前叫号变化窗口显示票号记录状态改为 CALLED重复抢号两个窗口同时对同一张票叫号只有第一个成功第二个收到「该号已被呼叫」服务端重启杀掉 server.jar 再启动客户端半分钟内自动重连队列从 Oracle 恢复乱码检查中文柜台名、业务类型名两端均 UTF-8无 ? 号这几项都过了演示就不会有大的尴尬。剩下的加分项按优先级排序把通信层从原生 Socket 改成 Netty 的 ChannelGroup展示你对框架迁移的理解把 GUI 从 Swing 换成 JavaFX 做触摸屏适配适合展厅环境持久层用 MyBatis 重写让 SQL 和 Java 代码分离连接池换成 HikariCP 并配置最小空闲数。但每一项的前提都是原生 Socket 版已经能讲清楚粘包怎么处理、心跳怎么保活、并发怎么防重。我印象最深的教训是有一版方案图省事让大屏客户端每 5 秒主动拉一次队列快照演示时大屏延迟肉眼可见被评审追问「为什么不用推送」时只能承认设计失误。后来改成服务器主动广播延迟降到毫秒级代码反而更简单了。这类系统的性能瓶颈从来不在 Oracle而在连接管理和状态同步的方式——消息是推还是拉决定了整个系统的实时性上限。希望这个判断对你做选型有帮助希望帮到你。本文还有配套的精品资源点击获取
返回列表