Làm thế nào để triển khai một ứng dụng Java đơn lẻ?


89

Đôi khi tôi thấy nhiều ứng dụng như msn, windows media player, v.v. là các ứng dụng đơn lẻ (khi người dùng thực thi trong khi ứng dụng đang chạy một phiên bản ứng dụng mới sẽ không được tạo).

Trong C #, tôi sử dụng Mutexlớp cho việc này nhưng tôi không biết cách thực hiện việc này trong Java.


Một cách tiếp cận rất đơn giản với java NIO xem ví dụ hoàn chỉnh stackoverflow.com/a/20015771/185022
AZ_

Câu trả lời:


62

Nếu tôi tin bài viết này , bởi:

lần đầu tiên cố gắng mở một ổ cắm lắng nghe trên giao diện localhost. Nếu nó có thể mở ổ cắm, thì có thể giả định rằng đây là phiên bản đầu tiên của ứng dụng được khởi chạy. Nếu không, giả định là một phiên bản của ứng dụng này đã chạy. Phiên bản mới phải thông báo cho phiên bản hiện có rằng đã thử khởi chạy, sau đó thoát. Phiên bản hiện tại sẽ tiếp quản sau khi nhận được thông báo và kích hoạt một sự kiện cho người nghe xử lý hành động.

Lưu ý: Ahe đề cập trong nhận xét rằng việc sử dụng InetAddress.getLocalHost()có thể phức tạp:

  • nó không hoạt động như mong đợi trong môi trường DHCP vì địa chỉ trả về phụ thuộc vào việc máy tính có quyền truy cập mạng hay không.
    Giải pháp là mở kết nối với InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
    Có thể liên quan đến lỗi 4435662 .
  • Tôi cũng tìm thấy lỗi 4665037 báo cáo kết quả mong đợi hơn getLocalHost: trả về địa chỉ IP của máy, so với kết quả thực tế: trả về 127.0.0.1.

thật ngạc nhiên khi có sự getLocalHosttrở lại 127.0.0.1trên Linux nhưng không phải trên windows.


Hoặc bạn có thể sử dụng ManagementFactoryđối tượng. Như đã giải thích ở đây :

Các getMonitoredVMs(int processPid)phương pháp nhận như tham số ứng dụng PID hiện tại, và nắm bắt những tên ứng dụng đó được gọi là từ dòng lệnh, ví dụ, các ứng dụng đã được bắt đầu từ c:\java\app\test.jarcon đường, sau đó biến giá trị là " c:\\java\\app\\test.jar". Bằng cách này, chúng tôi sẽ chỉ bắt tên ứng dụng trên dòng 17 của mã bên dưới.
Sau đó, chúng tôi tìm kiếm trong JVM một quy trình khác có cùng tên, nếu chúng tôi tìm thấy nó và PID ứng dụng khác nhau, điều đó có nghĩa là phiên bản ứng dụng thứ hai.

JNLP cũng cung cấp một SingleInstanceListener


3
Cần biết rằng giải pháp nắm tay có một lỗi. Gần đây chúng tôi đã phát hiện ra rằng điều InetAddress.getLocalHost()đó không hoạt động như mong đợi trong môi trường DHCP vì địa chỉ trả về phụ thuộc vào việc máy tính có quyền truy cập mạng hay không. Giải pháp là mở kết nối với InetAddress.getByAddress(new byte[] {127, 0, 0, 1});.
Ahe

2
@Ahe: điểm xuất sắc. Tôi đã bao gồm nhận xét của bạn cũng như các tham chiếu báo cáo lỗi Oracle-Sun trong câu trả lời đã chỉnh sửa của tôi.
VonC

3
Theo JavaDoc InetAddress.getByName(null)trả về địa chỉ của giao diện vòng lặp trở lại. Tôi đoán điều này sẽ tốt hơn khi chỉ định 127.0.0.1 theo cách thủ công vì về lý thuyết, điều này cũng sẽ hoạt động trong môi trường chỉ IPv6.
kayahr


1
@Puce Chắc chắn rồi, không vấn đề gì: Tôi đã khôi phục các liên kết đó.
VonC

65

Tôi sử dụng phương pháp sau trong phương thức chính. Đây là phương pháp đơn giản nhất, mạnh mẽ nhất và ít xâm phạm nhất mà tôi từng thấy nên tôi nghĩ rằng mình sẽ chia sẻ nó.

private static boolean lockInstance(final String lockFile) {
    try {
        final File file = new File(lockFile);
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
        log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

tham số "lockFile" nên dành cho ứng dụng máy tính để bàn là gì? tên tệp ứng dụng jar? Làm thế nào về việc không có tệp jar chỉ có một số tệp lớp?
5YrsLaterDBA

2
Có thực sự cần thiết phải giải phóng khóa tệp theo cách thủ công và đóng tệp khi tắt máy không? Điều này không xảy ra tự động khi quá trình chết?
Natix

5
Nhưng điều gì sẽ xảy ra nếu mất điện và máy tính tắt mà không chạy móc tắt máy? Tệp sẽ tồn tại và ứng dụng sẽ không thể chạy được.
Petr Hudeček

6
@ PetrHudeček Không sao đâu. Bất kể ứng dụng kết thúc như thế nào, khóa tệp sẽ được phát hành. Nếu đó không phải là một lần tắt máy thích hợp, thì điều này thậm chí còn có lợi cho phép ứng dụng nhận ra điều này trong lần chạy tiếp theo. Trong mọi trường hợp: Khóa là những gì được tính, không phải là sự hiện diện của chính tệp. Nếu tệp vẫn còn ở đó, ứng dụng sẽ khởi chạy.
Chủ tịch Dreamspace

@Robert: Cảm ơn giải pháp của bạn, tôi đã sử dụng nó kể từ đó. Và ngay bây giờ, tôi đã mở rộng nó để giao tiếp với phiên bản đã tồn tại mà một phiên bản khác đã cố gắng khởi động - bằng cách sử dụng thư mục WatchService! stackoverflow.com/a/36772436/3500521
Chủ tịch Dreamspace

9

Nếu ứng dụng. có GUI, khởi chạy nó với JWS và sử dụng SingleInstanceService.

Cập nhật

Phần bổ trợ Java (bắt buộc đối với cả applet và ứng dụng JWS) đã được Oracle không dùng nữa và bị xóa khỏi JDK. Các nhà sản xuất trình duyệt đã xóa nó khỏi trình duyệt của họ.

Vì vậy, câu trả lời này không còn tồn tại. Chỉ để nó ở đây để cảnh báo những người đang xem tài liệu cũ.


2
Cũng lưu ý rằng có vẻ như cá thể đang chạy có thể được thông báo về các cá thể mới và các đối số của chúng giúp dễ dàng giao tiếp với một chương trình như vậy.
Thorbjørn Ravn Andersen

6

Vâng, đây là một câu trả lời thực sự phù hợp cho ứng dụng đơn lẻ eclipse RCP eclipse dưới đây là mã của tôi

trong application.java

if(!isFileshipAlreadyRunning()){
        MessageDialog.openError(display.getActiveShell(), "Fileship already running", "Another instance of this application is already running.  Exiting.");
        return IApplication.EXIT_OK;
    } 


private static boolean isFileshipAlreadyRunning() {
    // socket concept is shown at http://www.rbgrn.net/content/43-java-single-application-instance
    // but this one is really great
    try {
        final File file = new File("FileshipReserved.txt");
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        //log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
       // log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

5

Chúng tôi sử dụng khóa tệp cho việc này (lấy một khóa độc quyền trên một tệp ma thuật trong thư mục dữ liệu ứng dụng của người dùng), nhưng chúng tôi chủ yếu quan tâm đến việc ngăn nhiều phiên bản chạy.

Nếu bạn đang cố gắng để phiên bản thứ hai truyền dòng lệnh args, v.v. vào phiên bản đầu tiên, thì việc sử dụng kết nối socket trên localhost sẽ giết chết hai con chim bằng một viên đá. Thuật toán chung:

  • Khi khởi chạy, hãy thử mở trình nghe trên cổng XXXX trên máy chủ cục bộ
  • nếu thất bại, hãy mở một trình ghi vào cổng đó trên localhost và gửi args dòng lệnh, sau đó tắt máy
  • nếu không, hãy nghe trên cổng XXXXX trên localhost. Khi nhận được args dòng lệnh, hãy xử lý chúng như thể ứng dụng được khởi chạy bằng dòng lệnh đó.

5

Tôi đã tìm thấy một giải pháp, một lời giải thích hơi biếm họa, nhưng vẫn hoạt động trong hầu hết các trường hợp. Nó sử dụng tệp khóa cũ đơn giản để tạo nội dung, nhưng ở một góc nhìn hoàn toàn khác:

http://javalandscape.blogspot.com/2008/07/single-instance-from-your-application.html

Tôi nghĩ rằng nó sẽ giúp ích cho những người có thiết lập tường lửa nghiêm ngặt.


Có, đó là một cách tốt vì khóa sẽ được phát hành nếu ứng dụng bị lỗi hoặc lâu hơn :)
LE GALL Benoît

5

Bạn có thể sử dụng thư viện JUnique. Nó cung cấp hỗ trợ để chạy ứng dụng java đơn phiên bản và là mã nguồn mở.

http://www.sauronsoftware.it/projects/junique/

Thư viện JUnique có thể được sử dụng để ngăn người dùng chạy cùng lúc nhiều phiên bản của cùng một ứng dụng Java.

JUnique triển khai các khóa và các kênh giao tiếp được chia sẻ giữa tất cả các phiên bản JVM được khởi chạy bởi cùng một người dùng.

public static void main(String[] args) {
    String appId = "myapplicationid";
    boolean alreadyRunning;
    try {
        JUnique.acquireLock(appId, new MessageHandler() {
            public String handle(String message) {
                // A brand new argument received! Handle it!
                return null;
            }
        });
        alreadyRunning = false;
    } catch (AlreadyLockedException e) {
        alreadyRunning = true;
    }
    if (!alreadyRunning) {
        // Start sequence here
    } else {
        for (int i = 0; i < args.length; i++) {
            JUnique.sendMessage(appId, args[0]));
        }
    }
}

Dưới mui xe, nó tạo khóa tệp trong thư mục% USER_DATA% /. Junique và tạo một ổ cắm máy chủ tại cổng ngẫu nhiên cho mỗi appId duy nhất cho phép gửi / nhận tin nhắn giữa các ứng dụng java.


Tôi có thể sử dụng điều này để ngăn chặn nhiều phiên bản ứng dụng java trong mạng không? aka, chỉ có một thể hiện của ứng dụng của tôi được phép trong toàn bộ mạng của tôi
Wuaner


2

Chi tiết về lớp ManagementFactory được hỗ trợ trong J2SE 5.0 trở lên

nhưng bây giờ tôi sử dụng J2SE 1.4 và tôi tìm thấy cái này http://audiprimadhanty.wordpress.com/2008/06/30/ensuring-one-instance-of-application-running-at-one-time/ nhưng tôi không bao giờ kiểm tra. Bạn nghĩ gì về nó?


Tôi tin rằng về cơ bản nó là những gì được mô tả trong liên kết đầu tiên của câu trả lời của tôi ở trên ... rbgrn.net/blog/2008/05/java-single-application-instance.html
VonC

2

Bạn có thể thử sử dụng API Tùy chọn. Nó độc lập với nền tảng.


Tôi thích ý tưởng này vì API rất đơn giản nhưng có thể một số trình quét vi rút sẽ không muốn bạn thay đổi sổ đăng ký để bạn gặp phải các vấn đề tương tự như khi sử dụng RMI trên các hệ thống có tường lửa phần mềm .... không chắc.
Cal

@Cal Nhưng vấn đề tương tự là với việc thay đổi / khóa tệp / vv ... bạn có nghĩ vậy không?
Alex,

2

Một cách chung chung hơn để giới hạn số lượng phiên bản trên một máy hoặc thậm chí toàn bộ mạng, là sử dụng ổ cắm đa hướng.

Sử dụng ổ cắm đa hướng, cho phép bạn phát thông báo tới bất kỳ phiên bản nào của ứng dụng của bạn, một số phiên bản có thể nằm trên các máy từ xa vật lý trong mạng công ty.

Bằng cách này, bạn có thể kích hoạt nhiều loại cấu hình, để kiểm soát những thứ như

  • Một hoặc nhiều phiên bản cho mỗi máy
  • Một hoặc Nhiều trường hợp trên mỗi mạng (ví dụ: kiểm soát lượt cài đặt trên một trang web khách hàng)

Hỗ trợ đa hướng của Java thông qua gói java.net với MulticastSocket & DatagramSocket là công cụ chính.

Lưu ý : MulticastSocket không đảm bảo cung cấp các gói dữ liệu, vì vậy bạn nên sử dụng một công cụ được xây dựng trên các ổ cắm đa hướng như JGroups . JGroups không giao hàng đảm bảo tất cả các dữ liệu. Nó là một tệp jar duy nhất, với một API rất đơn giản.

JGroups đã xuất hiện được một thời gian và có một số ứng dụng ấn tượng trong ngành, ví dụ như nó làm nền tảng cho cơ chế phân cụm của JBoss thực hiện truyền phát dữ liệu đến tất cả các phiên bản của một cụm.

Để sử dụng JGroups, để giới hạn số lượng phiên bản của một ứng dụng (trên máy hoặc mạng, giả sử: với số lượng giấy phép mà khách hàng đã mua) về mặt khái niệm rất đơn giản:

  • Khi khởi động ứng dụng của bạn, mỗi phiên bản cố gắng tham gia một nhóm được đặt tên, ví dụ: "Nhóm ứng dụng tuyệt vời của tôi". Bạn sẽ định cấu hình nhóm này để cho phép 0, 1 hoặc N thành viên
  • Khi số lượng thành viên trong nhóm lớn hơn những gì bạn đã định cấu hình cho nó .. ứng dụng của bạn sẽ từ chối khởi động.

1

Bạn có thể mở một tệp được ánh xạ bộ nhớ và sau đó xem tệp đó đã được MỞ chưa. nếu nó đã được mở, bạn có thể quay lại từ chính.

Các cách khác là sử dụng tệp khóa (thực hành unix tiêu chuẩn). Một cách nữa là bạn hãy đưa một thứ gì đó vào clipboard khi main khởi động sau khi kiểm tra xem đã có thứ gì đó trong clipboard chưa.

Ngoài ra, bạn có thể mở một ổ cắm ở chế độ lắng nghe (ServerSocket). Đầu tiên hãy thử kết nối với ổ cắm hte; nếu bạn không thể kết nối, hãy mở một tập máy chủ. nếu bạn kết nối, thì bạn biết rằng một phiên bản khác đang chạy.

Vì vậy, khá nhiều tài nguyên hệ thống có thể được sử dụng để biết rằng một ứng dụng đang chạy.

BR, ~ A


bạn có mã cho bất kỳ ý tưởng nào trong số đó không? Ngoài ra, điều gì sẽ xảy ra nếu tôi muốn rằng nếu người dùng bắt đầu một phiên bản mới, nó sẽ đóng tất cả những cái trước đó?
nhà phát triển android

1

Tôi đã sử dụng các ổ cắm cho việc đó và tùy thuộc vào việc ứng dụng ở phía máy khách hay phía máy chủ mà hành vi có một chút khác biệt:

  • phía máy khách: nếu một phiên bản đã tồn tại (tôi không thể lắng nghe trên một cổng cụ thể) tôi sẽ chuyển các tham số của ứng dụng và thoát (bạn có thể muốn thực hiện một số hành động trong trường hợp trước) nếu không, tôi sẽ khởi động ứng dụng.
  • phía máy chủ: nếu một phiên bản đã tồn tại, tôi sẽ in một thông báo và thoát ra, nếu không, tôi sẽ khởi động ứng dụng.

1
lớp công khai SingleInstance {
    public static final String LOCK = System.getProperty ("user.home") + File.separator + "test.lock";
    public static final String PIPE = System.getProperty ("user.home") + File.separator + "test.pipe";
    private static JFrame frame = null;

    public static void main (String [] args) {
        thử {
            FileChannel lockChannel = new RandomAccessFile (LOCK, "rw"). GetChannel ();
            FileLock flk = null; 
            thử {
                flk = lockChannel.tryLock ();
            } bắt (Có thể ném t) {
                t.printStackTrace ();
            }
            if (flk == null ||! flk.isValid ()) {
                System.out.println ("đang chạy, để lại thông báo đến ống dẫn và thoát ...");
                FileChannel pipeChannel = null;
                thử {
                    pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                    MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                    bb.put (0, (byte) 1);
                    bb.force ();
                } bắt (Có thể ném t) {
                    t.printStackTrace ();
                } cuối cùng {
                    if (pipeChannel! = null) {
                        thử {
                            pipeChannel.close ();
                        } bắt (Có thể ném t) {
                            t.printStackTrace ();
                        }
                    } 
                }
                System.exit (0);
            }
            // Chúng tôi không mở khóa và đóng kênh tại đây, 
            // sẽ được thực hiện sau khi ứng dụng bị treo hoặc đóng bình thường. 
            SwingUtilities.invokeLater (
                mới Runnable () {
                    public void run () {
                        createAndShowGUI ();
                    }
                }
            );

            FileChannel pipeChannel = null;
            thử {
                pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                trong khi (đúng) {
                    byte b = bb.get (0);
                    nếu (b> 0) {
                        bb.put (0, (byte) 0);
                        bb.force ();
                        SwingUtilities.invokeLater (
                            mới Runnable () {
                                public void run () {
                                    frame.setExtendedState (JFrame.NORMAL);
                                    frame.setAlwaysOnTop (true);
                                    frame.toFront ();
                                    frame.setAlwaysOnTop (sai);
                                }
                            }
                        );
                    }
                    Thread.sleep (1000);
                }
            } bắt (Có thể ném t) {
                t.printStackTrace ();
            } cuối cùng {
                if (pipeChannel! = null) {
                    thử {
                        pipeChannel.close ();
                    } bắt (Có thể ném t) {
                        t.printStackTrace ();
                    } 
                } 
            }
        } bắt (Có thể ném t) {
            t.printStackTrace ();
        } 
    }

    public static void createAndShowGUI () {

        frame = new JFrame ();
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        frame.setSize (800, 650);
        frame.getContentPane (). add (new JLabel ("MAIN WINDOW", 
                    SwingConstants.CENTER), BorderLayout.CENTER);
        frame.setLocationRelativeTo (null);
        frame.setVible (true);
    }
}


1

CHỈNH SỬA : Thay vì sử dụng phương pháp WatchService này, một chuỗi bộ đếm thời gian 1 giây đơn giản có thể được sử dụng để kiểm tra xem indicatorFile.exists (). Xóa nó, sau đó đưa ứng dụng vềFront ().

CHỈNH SỬA : Tôi muốn biết lý do tại sao điều này bị từ chối. Đó là giải pháp tốt nhất mà tôi đã thấy cho đến nay. Ví dụ: cách tiếp cận ổ cắm máy chủ không thành công nếu một ứng dụng khác tình cờ đang lắng nghe cổng.

Chỉ cần tải xuống Microsoft Windows Sysinternals TCPView (hoặc sử dụng netstat), khởi động nó, sắp xếp theo "Trạng thái", tìm khối dòng có nội dung "LISTENING", chọn một khối có địa chỉ từ xa cho biết tên máy tính của bạn, đặt cổng đó vào Ổ cắm mới của bạn ()-giải pháp. Khi thực hiện nó, tôi có thể gặp thất bại bất cứ lúc nào. Và nó hợp lý , bởi vì nó là nền tảng của cách tiếp cận. Hoặc những gì tôi không nhận được về cách thực hiện điều này?

Xin vui lòng thông báo cho tôi nếu và làm thế nào tôi sai về điều này!

Quan điểm của tôi - mà tôi đang yêu cầu bạn bác bỏ nếu có thể - là các nhà phát triển đang được khuyên sử dụng một cách tiếp cận trong mã sản xuất sẽ không thành công trong ít nhất 1 trong số khoảng 60000 trường hợp. Và nếu quan điểm này là đúng, thì hoàn toàn có thể không có giải pháp được trình bày không có vấn đề này bị phản đối và bị chỉ trích vì số lượng mã của nó.

Nhược điểm của phương pháp tiếp cận socket so với:

  • Không thành công nếu chọn sai vé số (số cổng).
  • Không thành công trong môi trường nhiều người dùng: Chỉ một người dùng có thể chạy ứng dụng cùng một lúc. (Cách tiếp cận của tôi sẽ phải thay đổi một chút để tạo (các) tệp trong cây người dùng, nhưng điều đó không đáng kể.)
  • Không thành công nếu các quy tắc tường lửa quá nghiêm ngặt.
  • Khiến những người dùng đáng ngờ (mà tôi đã gặp trong tự nhiên) tự hỏi bạn đang nghĩ ra trò tai quái gì khi trình soạn thảo văn bản của bạn yêu cầu một ổ cắm máy chủ.

Tôi vừa có một ý tưởng hay về cách giải quyết vấn đề giao tiếp Java phiên bản mới đến phiên bản hiện tại theo cách có thể hoạt động trên mọi hệ thống. Vì vậy, tôi đã hoàn thành lớp học này trong khoảng hai giờ. Hoạt động như một cái duyên: D

Nó dựa trên phương pháp khóa tệp của Robert (cũng trên trang này), mà tôi đã sử dụng kể từ đó. Để thông báo cho phiên bản đang chạy rằng một phiên bản khác đã cố gắng khởi động (nhưng không) ... một tệp được tạo và xóa ngay lập tức, và phiên bản đầu tiên sử dụng WatchService để phát hiện sự thay đổi nội dung thư mục này. Tôi không thể tin rằng rõ ràng đây là một ý tưởng mới, với căn bản của vấn đề.

Điều này có thể dễ dàng được thay đổi để chỉ tạo và không xóa tệp, và sau đó thông tin có thể được đưa vào đó để cá thể thích hợp có thể đánh giá, ví dụ: đối số dòng lệnh - và cá thể thích hợp sau đó có thể thực hiện xóa. Cá nhân tôi chỉ cần biết khi nào cần khôi phục cửa sổ ứng dụng của mình và gửi nó về phía trước.

Ví dụ sử dụng:

public static void main(final String[] args) {

    // ENSURE SINGLE INSTANCE
    if (!SingleInstanceChecker.INSTANCE.isOnlyInstance(Main::otherInstanceTriedToLaunch, false)) {
        System.exit(0);
    }

    // launch rest of application here
    System.out.println("Application starts properly because it's the only instance.");
}

private static void otherInstanceTriedToLaunch() {
    // Restore your application window and bring it to front.
    // But make sure your situation is apt: This method could be called at *any* time.
    System.err.println("Deiconified because other instance tried to start.");
}

Đây là lớp học:

package yourpackagehere;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.file.*;




/**
 * SingleInstanceChecker v[(2), 2016-04-22 08:00 UTC] by dreamspace-president.com
 * <p>
 * (file lock single instance solution by Robert https://stackoverflow.com/a/2002948/3500521)
 */
public enum SingleInstanceChecker {

    INSTANCE; // HAHA! The CONFUSION!


    final public static int POLLINTERVAL = 1000;
    final public static File LOCKFILE = new File("SINGLE_INSTANCE_LOCKFILE");
    final public static File DETECTFILE = new File("EXTRA_INSTANCE_DETECTFILE");


    private boolean hasBeenUsedAlready = false;


    private WatchService watchService = null;
    private RandomAccessFile randomAccessFileForLock = null;
    private FileLock fileLock = null;


    /**
     * CAN ONLY BE CALLED ONCE.
     * <p>
     * Assumes that the program will close if FALSE is returned: The other-instance-tries-to-launch listener is not
     * installed in that case.
     * <p>
     * Checks if another instance is already running (temp file lock / shutdownhook). Depending on the accessibility of
     * the temp file the return value will be true or false. This approach even works even if the virtual machine
     * process gets killed. On the next run, the program can even detect if it has shut down irregularly, because then
     * the file will still exist. (Thanks to Robert https://stackoverflow.com/a/2002948/3500521 for that solution!)
     * <p>
     * Additionally, the method checks if another instance tries to start. In a crappy way, because as awesome as Java
     * is, it lacks some fundamental features. Don't worry, it has only been 25 years, it'll sure come eventually.
     *
     * @param codeToRunIfOtherInstanceTriesToStart Can be null. If not null and another instance tries to start (which
     *                                             changes the detect-file), the code will be executed. Could be used to
     *                                             bring the current (=old=only) instance to front. If null, then the
     *                                             watcher will not be installed at all, nor will the trigger file be
     *                                             created. (Null means that you just don't want to make use of this
     *                                             half of the class' purpose, but then you would be better advised to
     *                                             just use the 24 line method by Robert.)
     *                                             <p>
     *                                             BE CAREFUL with the code: It will potentially be called until the
     *                                             very last moment of the program's existence, so if you e.g. have a
     *                                             shutdown procedure or a window that would be brought to front, check
     *                                             if the procedure has not been triggered yet or if the window still
     *                                             exists / hasn't been disposed of yet. Or edit this class to be more
     *                                             comfortable. This would e.g. allow you to remove some crappy
     *                                             comments. Attribution would be nice, though.
     * @param executeOnAWTEventDispatchThread      Convenience function. If false, the code will just be executed. If
     *                                             true, it will be detected if we're currently on that thread. If so,
     *                                             the code will just be executed. If not so, the code will be run via
     *                                             SwingUtilities.invokeLater().
     * @return if this is the only instance
     */
    public boolean isOnlyInstance(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        if (hasBeenUsedAlready) {
            throw new IllegalStateException("This class/method can only be used once, which kinda makes sense if you think about it.");
        }
        hasBeenUsedAlready = true;

        final boolean ret = canLockFileBeCreatedAndLocked();

        if (codeToRunIfOtherInstanceTriesToStart != null) {
            if (ret) {
                // Only if this is the only instance, it makes sense to install a watcher for additional instances.
                installOtherInstanceLaunchAttemptWatcher(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread);
            } else {
                // Only if this is NOT the only instance, it makes sense to create&delete the trigger file that will effect notification of the other instance.
                //
                // Regarding "codeToRunIfOtherInstanceTriesToStart != null":
                // While creation/deletion of the file concerns THE OTHER instance of the program,
                // making it dependent on the call made in THIS instance makes sense
                // because the code executed is probably the same.
                createAndDeleteOtherInstanceWatcherTriggerFile();
            }
        }

        optionallyInstallShutdownHookThatCleansEverythingUp();

        return ret;
    }


    private void createAndDeleteOtherInstanceWatcherTriggerFile() {

        try {
            final RandomAccessFile randomAccessFileForDetection = new RandomAccessFile(DETECTFILE, "rw");
            randomAccessFileForDetection.close();
            Files.deleteIfExists(DETECTFILE.toPath()); // File is created and then instantly deleted. Not a problem for the WatchService :)
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private boolean canLockFileBeCreatedAndLocked() {

        try {
            randomAccessFileForLock = new RandomAccessFile(LOCKFILE, "rw");
            fileLock = randomAccessFileForLock.getChannel().tryLock();
            return fileLock != null;
        } catch (Exception e) {
            return false;
        }
    }


    private void installOtherInstanceLaunchAttemptWatcher(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        // PREPARE WATCHSERVICE AND STUFF
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        final File appFolder = new File("").getAbsoluteFile(); // points to current folder
        final Path appFolderWatchable = appFolder.toPath();


        // REGISTER CURRENT FOLDER FOR WATCHING FOR FILE DELETIONS
        try {
            appFolderWatchable.register(watchService, StandardWatchEventKinds.ENTRY_DELETE);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }


        // INSTALL WATCHER THAT LOOKS IF OUR detectFile SHOWS UP IN THE DIRECTORY CHANGES. IF THERE'S A CHANGE, ANOTHER INSTANCE TRIED TO START, SO NOTIFY THE CURRENT ONE OF THAT.
        final Thread t = new Thread(() -> watchForDirectoryChangesOnExtraThread(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread));
        t.setDaemon(true);
        t.setName("directory content change watcher");
        t.start();
    }


    private void optionallyInstallShutdownHookThatCleansEverythingUp() {

        if (fileLock == null && randomAccessFileForLock == null && watchService == null) {
            return;
        }

        final Thread shutdownHookThread = new Thread(() -> {
            try {
                if (fileLock != null) {
                    fileLock.release();
                }
                if (randomAccessFileForLock != null) {
                    randomAccessFileForLock.close();
                }
                Files.deleteIfExists(LOCKFILE.toPath());
            } catch (Exception ignore) {
            }
            if (watchService != null) {
                try {
                    watchService.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHookThread);
    }


    private void watchForDirectoryChangesOnExtraThread(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        while (true) { // To eternity and beyond! Until the universe shuts down. (Should be a volatile boolean, but this class only has absolutely required features.)

            try {
                Thread.sleep(POLLINTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            final WatchKey wk;
            try {
                wk = watchService.poll();
            } catch (ClosedWatchServiceException e) {
                // This situation would be normal if the watcher has been closed, but our application never does that.
                e.printStackTrace();
                return;
            }

            if (wk == null || !wk.isValid()) {
                continue;
            }


            for (WatchEvent<?> we : wk.pollEvents()) {

                final WatchEvent.Kind<?> kind = we.kind();
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    System.err.println("OVERFLOW of directory change events!");
                    continue;
                }


                final WatchEvent<Path> watchEvent = (WatchEvent<Path>) we;
                final File file = watchEvent.context().toFile();


                if (file.equals(DETECTFILE)) {

                    if (!executeOnAWTEventDispatchThread || SwingUtilities.isEventDispatchThread()) {
                        codeToRunIfOtherInstanceTriesToStart.run();
                    } else {
                        SwingUtilities.invokeLater(codeToRunIfOtherInstanceTriesToStart);
                    }

                    break;

                } else {
                    System.err.println("THIS IS THE FILE THAT WAS DELETED: " + file);
                }

            }

            wk.reset();
        }
    }

}

Bạn không cần hàng trăm dòng mã để giải quyết vấn đề này. new ServerSocket()với một khối bắt khá đầy đủ,
Marquis of Lorne

@EJP Bạn đang đề cập đến câu trả lời được chấp nhận hay bạn đang nói về điều gì? Tôi đã tìm kiếm khá nhiều cho một giải pháp không có thư viện bổ sung của nền tảng x mà không thất bại, ví dụ như vì một số ổ cắm đã bị một ứng dụng khác chiếm giữ . Nếu có một giải pháp cho điều này - đặc biệt là siêu đơn giản như bạn đang đề cập - thì tôi muốn biết về nó.
Chủ tịch Dreamspace

@EJP: Tôi muốn hỏi lại 1) giải pháp tầm thường mà bạn đang đề cập đến là bạn đã bị treo lủng lẳng như củ cà rốt trước đầu tôi, 2) trong trường hợp đó là giải pháp ổ cắm, câu trả lời được chấp nhận bắt đầu bằng, nếu một hoặc nhiều áp dụng gạch đầu dòng "Nhược điểm của phương pháp tiếp cận ổ cắm" và 3) nếu vậy, tại sao bất chấp những thiếu sót đó, bạn vẫn đề xuất cách tiếp cận đó hơn một phương pháp như của tôi.
Chủ tịch Dreamspace

@EJP: Vấn đề là giọng nói của bạn có trọng lượng khá lớn, như bạn chắc chắn biết, nhưng tất cả bằng chứng mà tôi có được đều buộc tôi phải tin rằng lời khuyên của bạn ở đây là sai. Thấy chưa, tôi không khăng khăng rằng giải pháp của tôi là đúng và tất cả những điều đó, nhưng tôi là một cỗ máy dựa trên bằng chứng. Bạn không thấy rằng vị trí của bạn mang lại cho bạn trách nhiệm với cộng đồng để điền vào những mảnh ghép còn thiếu của cuộc giao tiếp mà bạn đã bắt đầu này?
Chủ tịch Dreamspace

@EJP: Vì đáng buồn là không có phản ứng nào từ bạn, đây là điều tôi sẽ coi là sự thật: Sự thật về giải pháp socket máy chủ là nó thực sự rất thiếu sót và lý do cho hầu hết những người chọn nó có thể là "Những người khác sử dụng cái này. ", hoặc họ có thể đã bị lừa sử dụng nó bởi những người vô trách nhiệm. Tôi đoán rằng một phần lý do tại sao bạn không đánh giá cao chúng tôi với những lời giải thích bắt buộc là bạn không thể hiểu được điều đó / tại sao bạn chưa bao giờ đặt câu hỏi về cách tiếp cận này và bạn không muốn công khai điều này.
Chủ tịch Dreamspace

1

Thư viện Unique4j có thể được sử dụng để chạy một phiên bản ứng dụng Java và chuyển các thông điệp. Bạn có thể xem nó tại https://github.com/prat-man/unique4j . Nó hỗ trợ Java 1.6+.

Nó sử dụng sự kết hợp của khóa tệp và khóa cổng động để phát hiện và giao tiếp giữa các phiên bản với mục tiêu chính là chỉ cho phép một phiên bản chạy.

Sau đây là một ví dụ đơn giản về tương tự:

import tk.pratanumandal.unique4j.Unique4j;
import tk.pratanumandal.unique4j.exception.Unique4jException;

public class Unique4jDemo {

    // unique application ID
    public static String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";

    public static void main(String[] args) throws Unique4jException, InterruptedException {

        // create unique instance
        Unique4j unique = new Unique4j(APP_ID) {
            @Override
            public void receiveMessage(String message) {
                // display received message from subsequent instance
                System.out.println(message);
            }

            @Override
            public String sendMessage() {
                // send message to first instance
                return "Hello World!";
            }
        };

        // try to obtain lock
        boolean lockFlag = unique.acquireLock();

        // sleep the main thread for 30 seconds to simulate long running tasks
        Thread.sleep(30000);

        // try to free the lock before exiting program
        boolean lockFreeFlag = unique.freeLock();

    }

}

Tuyên bố từ chối trách nhiệm: Tôi đã tạo và duy trì thư viện Unique4j.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.