단일 인스턴스 Java 애플리케이션을 구현하는 방법은 무엇입니까?


89

때때로 단일 인스턴스 응용 프로그램 인 msn, Windows Media Player 등과 같은 많은 응용 프로그램을 볼 수 있습니다 (응용 프로그램이 실행되는 동안 사용자가 실행하면 새 응용 프로그램 인스턴스가 생성되지 않습니다).

C #에서는이를 위해 Mutex클래스를 사용 하지만 Java에서이 작업을 수행하는 방법을 모릅니다.


Java NIO를 사용한 매우 간단한 접근 방식은 전체 예제 참조 stackoverflow.com/a/20015771/185022
AZ_

답변:


62

나는이 생각되면 작성자 :

첫 번째 인스턴스가 localhost 인터페이스에서 청취 소켓을 열려고 시도하도록합니다. 소켓을 열 수있는 경우 이것이 시작될 애플리케이션의 첫 번째 인스턴스라고 가정합니다. 그렇지 않은 경우이 응용 프로그램의 인스턴스가 이미 실행 중이라고 가정합니다. 새 인스턴스는 기존 인스턴스에 시작이 시도되었음을 알리고 종료해야합니다. 알림을받은 후 기존 인스턴스가 인계 받아 작업을 처리하는 리스너에 이벤트를 발생시킵니다.

참고 : Ahe 는 주석에서 사용 InetAddress.getLocalHost()이 까다로울 수 있다고 언급합니다 .

  • 반환 된 주소는 컴퓨터에 네트워크 액세스 권한이 있는지 여부에 따라 달라지기 때문에 DHCP 환경에서 예상대로 작동하지 않습니다.
    해결책은 연결을 여는 것이 었습니다 InetAddress.getByAddress(new byte[] {127, 0, 0, 1}).
    아마도 버그 4435662 와 관련이 있을 것 입니다.
  • 나는 또한 예상 결과보다보고하는 버그 4665037 을 발견했다 getLocalHost: return IP address of machine, vs. Actual results : return 127.0.0.1.

Linux에서는 getLocalHost반환 127.0.0.1되지만 Windows 에서는 반환 되지 않는 것이 놀랍습니다 .


또는 ManagementFactory개체를 사용할 수 있습니다 . 여기에 설명 된대로 :

getMonitoredVMs(int processPid)메서드는 현재 응용 프로그램 PID를 매개 변수로 받고 명령 줄에서 호출 된 응용 프로그램 이름을 포착합니다 (예 : 응용 프로그램이 c:\java\app\test.jar경로에서 시작된 경우 값 변수는 " c:\\java\\app\\test.jar"). 이렇게하면 아래 코드의 17 행에서 애플리케이션 이름 만 잡을 수 있습니다.
그 후 JVM에서 동일한 이름을 가진 다른 프로세스를 검색합니다. 발견하고 애플리케이션 PID가 다르면 두 번째 애플리케이션 인스턴스임을 의미합니다.

JNLP는 또한 SingleInstanceListener


3
첫 번째 솔루션에는 버그가 있습니다. InetAddress.getLocalHost()반환 된 주소는 컴퓨터가 네트워크에 액세스 할 수 있는지 여부에 따라 달라지기 때문에 DHCP 환경에서 예상대로 작동하지 않는 것을 최근에 발견했습니다 . 해결책은와의 연결을 여는 것입니다 InetAddress.getByAddress(new byte[] {127, 0, 0, 1});.
Ahe

2
@Ahe : 훌륭한 지적입니다. 수정 된 답변에 Oracle-Sun 버그 보고서 참조뿐만 아니라 귀하의 의견을 포함했습니다.
VonC

3
JavaDoc에 따르면 InetAddress.getByName(null)루프백 인터페이스의 주소를 반환합니다. 이론상 IPv6 전용 환경에서도 작동해야하므로 127.0.0.1을 수동으로 지정하는 것보다 이것이 더 낫다고 생각합니다.
kayahr


1
@Puce 물론입니다. 문제 없습니다. 링크를 복원했습니다.
VonC 2015 년

65

나는 주요 방법에서 다음 방법을 사용합니다. 이것은 내가 본 것 중 가장 간단하고, 가장 강력하며, 가장 덜 방해가되는 방법이므로 공유 할 것이라고 생각했습니다.

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;
}

데스크탑 애플리케이션의 "lockFile"매개 변수는 무엇입니까? 응용 프로그램 jar 파일 이름? jar 파일이없는 클래스 파일은 어떻습니까?
5YrsLaterDBA

2
수동으로 파일 잠금을 해제하고 종료시 파일을 닫아야합니까? 프로세스가 종료되면 자동으로 발생하지 않습니까?
Natix 2014 년

5
그러나 전원이 꺼지고 종료 후크를 실행하지 않고 컴퓨터가 종료되면 어떻게됩니까? 파일이 유지되고 응용 프로그램을 실행할 수 없습니다.
Petr Hudeček 2014

6
@ PetrHudeček 괜찮아요. 응용 프로그램이 어떻게 종료 되더라도 파일 잠금이 해제됩니다. 적절한 종료가 아니었다면 응용 프로그램이 다음 실행에서이를 인식 할 수 있도록하는 이점이 있습니다. 어쨌든 : 잠금은 파일 자체의 존재가 아니라 중요한 것입니다. 파일이 아직 있으면 응용 프로그램이 실행됩니다.
Dreamspace President

@Robert : 솔루션에 감사드립니다. 그 이후로 계속 사용하고 있습니다. 그리고 지금 막 다른 인스턴스가 시작하려고했던 이미 존재하는 인스턴스와도 통신하도록 확장했습니다. WatchService! stackoverflow.com/a/36772436/3500521
Dreamspace President

10

앱이라면. GUI가 있고 JWS로 시작하고 SingleInstanceService.

최신 정보

Java 플러그인 (애플릿 및 JWS 앱 모두에 필요)은 Oracle에서 더 이상 사용되지 않으며 JDK에서 제거되었습니다. 브라우저 제조업체는 이미 브라우저에서이를 제거했습니다.

그래서이 대답은 없어졌습니다. 오래된 문서를 보는 사람들에게 경고하기 위해 여기에 두십시오.


2
또한 실행중인 인스턴스에 새 인스턴스와 해당 인수에 대한 정보를 제공하여 해당 프로그램과 쉽게 통신 할 수있는 것으로 보입니다.
Thorbjørn Ravn Andersen

6

예 이것은 이클립스 RCP 이클립스 단일 인스턴스 응용 프로그램에 대한 정말 괜찮은 대답입니다.

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

이를 위해 파일 잠금을 사용하지만 (사용자의 앱 데이터 디렉토리에있는 매직 파일에 대한 배타적 잠금을 확보) 주로 여러 인스턴스가 실행되는 것을 방지하는 데 관심이 있습니다.

두 번째 인스턴스가 명령 줄 인수 등을 첫 번째 인스턴스에 전달하도록하려는 경우 localhost에서 소켓 연결을 사용하면 한 돌로 두 마리의 새를 죽일 수 있습니다. 일반 알고리즘 :

  • 시작할 때 localhost의 포트 XXXX에서 리스너를 열어보십시오.
  • 실패하면 localhost의 해당 포트에 대한 작성기를 열고 명령 줄 인수를 보낸 다음 종료합니다.
  • 그렇지 않으면 localhost의 포트 XXXXX에서 수신 대기합니다. 명령 줄 인수를 받으면 해당 명령 줄로 앱이 시작된 것처럼 처리합니다.

5

나는 해결책, 약간의 만화적인 설명을 찾았지만 대부분의 경우 여전히 작동합니다. 그것은 물건을 만드는 평범한 오래된 잠금 파일을 사용하지만 완전히 다른 관점에서 :

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

방화벽 설정이 엄격한 분들에게 도움이 될 것 같습니다.


네, 응용 프로그램 : 충돌 또는 이렇게하면 잠금이 이벤트를 해제하는 것처럼 좋은 방법입니다
LE GALL 브누아

5

JUnique 라이브러리를 사용할 수 있습니다. 단일 인스턴스 Java 애플리케이션 실행을 지원하며 오픈 소스입니다.

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

JUnique 라이브러리는 사용자가 동일한 Java 애플리케이션의 더 많은 인스턴스를 동시에 실행하는 것을 방지하는 데 사용할 수 있습니다.

JUnique는 동일한 사용자가 시작한 모든 JVM 인스턴스간에 공유되는 잠금 및 통신 채널을 구현합니다.

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]));
        }
    }
}

내부적으로는 % USER_DATA % /. junique 폴더에 파일 잠금을 생성하고 Java 응용 프로그램간에 메시지를주고받을 수 있도록 고유 한 각 appId에 대해 임의의 포트에 서버 소켓을 생성합니다.


네트워크에서 Java 응용 프로그램의 다중 인스턴스를 방지하기 위해 이것을 사용할 수 있습니까? 일명, 내 응용 프로그램의 하나의 인스턴스 만이 내 전체 네트워크 내에서 허용된다
Wuaner


2

J2SE 5.0 이상에서 지원되는 ManagementFactory 클래스 세부 사항

하지만 지금은 J2SE 1.4를 사용하고 http://audiprimadhanty.wordpress.com/2008/06/30/ensuring-one-instance-of-application-running-at-one-time/을 찾았 지만 테스트하지 않습니다. 당신이 그것에 대해 어떻게 생각하십니까?


나는 그것이 본질적으로 위의 내 대답의 첫 번째 링크에 설명되어 있다고 생각합니다 ... rbgrn.net/blog/2008/05/java-single-application-instance.html
VonC

2

Preferences API를 사용해 볼 수 있습니다. 플랫폼 독립적입니다.


API가 간단하기 때문에이 아이디어가 마음에 들지만 일부 바이러스 스캐너는 레지스트리 변경을 원하지 않아 소프트웨어 방화벽이있는 시스템에서 RMI를 사용할 때와 유사한 문제가 발생합니다 .... 확실하지 않습니다.
Cal

@Cal하지만 같은 문제는 파일 변경 / 잠금 / 기타입니다 ... 그렇지 않습니까?
Alex

2

단일 시스템 또는 전체 네트워크에서 인스턴스 수를 제한하는보다 일반적인 방법은 멀티 캐스트 소켓을 사용하는 것입니다.

멀티 캐스트 소켓을 사용하면 애플리케이션의 모든 인스턴스에 메시지를 브로드 캐스트 할 수 있으며, 그중 일부는 회사 네트워크를 통해 물리적으로 원격 시스템에있을 수 있습니다.

이러한 방식으로 여러 유형의 구성을 활성화하여 다음과 같은 항목을 제어 할 수 있습니다.

  • 머신 당 하나 또는 여러 인스턴스
  • 네트워크 당 하나 또는 여러 인스턴스 (예 : 클라이언트 사이트에서 설치 제어)

Java의 멀티 캐스트 지원은 MulticastSocketDatagramSocket 이 주요 도구 인 java.net 패키지 를 통해 이루어 집니다.

참고 : MulticastSocket은 데이터 패킷의 전달을 보장하지 않으므로 JGroups 와 같은 멀티 캐스트 소켓 위에 구축 된 도구를 사용해야합니다 . JGroups 모든 데이터의 전달을 보장합니다. 매우 간단한 API가있는 단일 jar 파일입니다.

JGroups는 한동안 업계에서 인상적인 용도로 사용되었습니다. 예를 들어 JBoss의 클러스터링 메커니즘이 클러스터의 모든 인스턴스에 데이터를 브로드 캐스트하는 것을 뒷받침합니다.

JGroups를 사용하여 앱의 인스턴스 수를 제한하는 것은 개념적으로 매우 간단합니다.

  • 애플리케이션을 시작할 때 각 인스턴스는 "My Great App Group"과 같은 명명 된 그룹에 참여하려고합니다. 0, 1 또는 N 명의 구성원을 허용하도록이 그룹을 구성합니다.
  • 그룹 구성원 수가 구성한 것보다 많으면 앱 시작을 거부해야합니다.

1

메모리 매핑 파일을 열고 해당 파일이 이미 열려 있는지 확인할 수 있습니다. 이미 열려 있으면 메인에서 돌아올 수 있습니다.

다른 방법은 잠금 파일을 사용하는 것입니다 (표준 유닉스 관행). 한 가지 더 방법은 클립 보드에 이미 무언가가 있는지 확인한 후 main이 시작될 때 클립 보드에 무언가를 넣는 것입니다.

그렇지 않으면 청취 모드 (ServerSocket)에서 소켓을 열 수 있습니다. 먼저 소켓에 연결을 시도하십시오. 연결할 수 없으면 serversocket을 엽니 다. 연결하면 다른 인스턴스가 이미 실행 중임을 알 수 있습니다.

따라서 앱이 실행 중인지 확인하기 위해 거의 모든 시스템 리소스를 사용할 수 있습니다.

BR, ~ A


그 아이디어에 대한 코드가 있습니까? 또한 사용자가 새 인스턴스를 시작하면 이전 인스턴스를 모두 닫으려면 어떻게해야합니까?
안드로이드 개발자

1

이를 위해 소켓을 사용했으며 응용 프로그램이 클라이언트 측인지 서버 측인지에 따라 동작이 약간 다릅니다.

  • 클라이언트 측 : 인스턴스가 이미 존재하는 경우 (특정 포트에서 수신 할 수 없음) 애플리케이션 매개 변수를 전달하고 종료 (이전 인스턴스에서 일부 작업을 수행 할 수 있음)하지 않으면 애플리케이션을 시작합니다.
  • 서버 측 : 인스턴스가 이미 있으면 메시지를 인쇄하고 종료하고, 그렇지 않으면 응용 프로그램을 시작합니다.

1
공용 클래스 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";
    개인 정적 JFrame 프레임 = null;

    public static void main (String [] args) {
        {
            FileChannel lockChannel = new RandomAccessFile (LOCK, "rw"). getChannel ();
            FileLock flk = null; 
            {
                flk = lockChannel.tryLock ();
            } catch (Throwable t) {
                t.printStackTrace ();
            }
            if (flk == null ||! flk.isValid ()) {
                System.out.println ( "이미 실행 중입니다. 파이프에 메시지를 남기고 종료 중 ...");
                FileChannel pipeChannel = null;
                {
                    pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                    MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                    bb.put (0, (바이트) 1);
                    bb.force ();
                } catch (Throwable t) {
                    t.printStackTrace ();
                } 드디어 {
                    if (pipeChannel! = null) {
                        {
                            pipeChannel.close ();
                        } catch (Throwable t) {
                            t.printStackTrace ();
                        }
                    } 
                }
                System.exit (0);
            }
            // 여기서 잠금을 해제하고 채널을 닫지 않습니다. 
            // 애플리케이션이 충돌하거나 정상적으로 종료 된 후에 수행됩니다. 
            SwingUtilities.invokeLater (
                new Runnable () {
                    public void run () {
                        createAndShowGUI ();
                    }
                }
            );

            FileChannel pipeChannel = null;
            {
                pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                while (true) {
                    바이트 b = bb.get (0);
                    if (b> 0) {
                        bb.put (0, (바이트) 0);
                        bb.force ();
                        SwingUtilities.invokeLater (
                            new Runnable () {
                                public void run () {
                                    frame.setExtendedState (JFrame.NORMAL);
                                    frame.setAlwaysOnTop (true);
                                    frame.toFront ();
                                    frame.setAlwaysOnTop (false);
                                }
                            }
                        );
                    }
                    Thread.sleep (1000);
                }
            } catch (Throwable t) {
                t.printStackTrace ();
            } 드디어 {
                if (pipeChannel! = null) {
                    {
                        pipeChannel.close ();
                    } catch (Throwable t) {
                        t.printStackTrace ();
                    } 
                } 
            }
        } catch (Throwable t) {
            t.printStackTrace ();
        } 
    }

    public static void createAndShowGUI () {

        프레임 = 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.setVisible (true);
    }
}


1

편집 :이 WatchService 접근 방식을 사용하는 대신 간단한 1 초 타이머 스레드를 사용하여 indicatorFile.exists ()가 있는지 확인할 수 있습니다. 삭제 한 다음 응용 프로그램을 Front ()로 가져옵니다.

편집 : 왜 이것이 비추천인지 알고 싶습니다. 지금까지 본 최고의 솔루션입니다. 예를 들어 다른 응용 프로그램이 이미 포트를 수신하고있는 경우 서버 소켓 접근이 실패합니다.

Microsoft Windows Sysinternals TCPView를 다운로드 (또는 netstat 사용)하고, 시작하고, "상태"별로 정렬하고, "LISTENING"이라고 표시된 줄 블록을 찾고, 원격 주소에 컴퓨터 이름이 표시된 것을 선택하고, 해당 포트를 새 소켓에 넣습니다. ()-해결책. 그것을 구현할 때 매번 실패를 일으킬 수 있습니다. 그리고 그것은 접근 방식의 근간이기 때문에 논리적 입니다. 아니면 이것을 구현하는 방법과 관련하여 무엇을 얻지 못합니까?

이것에 대해 내가 틀렸다면 알려주십시오!

가능한 한 반박 해달라고 요청하는 내 견해는 개발자가 약 60000 건 중 적어도 1 건에서 실패하는 프로덕션 코드에서 접근 방식을 사용하도록 조언 받고 있다는 것입니다. 그리고이 견해가 옳다면, 이 문제가없는 제시된 솔루션이 코드 양에 대해 비판을 받고 비판을받는 것은 절대로 아닐 수 있습니다.

비교에서 소켓 접근 방식의 단점 :

  • 잘못된 복권 (항구 번호)을 선택하면 실패합니다.
  • 다중 사용자 환경에서 실패 : 한 사용자 만 동시에 애플리케이션을 실행할 수 있습니다. (사용자 트리에 파일을 생성하려면 내 접근 방식을 약간 변경해야하지만 이는 사소한 일입니다.)
  • 방화벽 규칙이 너무 엄격한 경우 실패합니다.
  • 의심스러운 사용자 (내가 야생에서 만났음)는 텍스트 편집기가 서버 소켓을 요구할 때 무슨 헛소리를하는지 궁금해합니다.

모든 시스템에서 작동하는 방식으로 새로운 인스턴스-기존 인스턴스 Java 통신 문제를 해결하는 방법에 대한 좋은 아이디어가있었습니다. 그래서 저는이 수업을 약 2 시간 만에 끝냈습니다. 매력처럼 작동 : D

이것은 Robert 의 파일 잠금 접근 방식 (또한이 페이지에 있음)을 기반으로 합니다. 이미 실행중인 인스턴스에 다른 인스턴스가 시작을 시도했지만 시작하지 않았 음을 알리기 위해 ... 파일이 생성되고 즉시 삭제되며 첫 번째 인스턴스는 WatchService를 사용하여이 폴더 내용 변경을 감지합니다. 문제가 얼마나 근본적인지를 감안할 때 이것이 새로운 아이디어라는 것을 믿을 수 없습니다.

이것은 그냥 만들기 위해 쉽게 변경할 수 있습니다파일을 하고 삭제하지 않도록 수 있으며, 적절한 인스턴스가 평가할 수있는 정보 (예 : 명령 줄 인수)를 입력 할 수 있으며 적절한 인스턴스가 삭제를 수행 할 수 있습니다. 개인적으로 애플리케이션의 창을 복원하고 앞으로 보낼 때만 알면됩니다.

사용 예 :

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.");
}

수업은 다음과 같습니다.

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

}

이 문제를 해결하기 위해 수백 줄의 코드가 필요하지 않습니다. new ServerSocket()캐치 블록을 사용하는 것이 매우 적절합니다.
Marquis of Lorne

@EJP 당신은 받아 들여진 대답을 언급하고 있습니까, 아니면 무엇에 대해 이야기하고 있습니까? 예를 들어 일부 소켓이 이미 다른 응용 프로그램에 의해 점유되고 있기 때문에 실패하지 않는 x-platform no-extra-library 솔루션을 꽤 많이 검색했습니다 . 이것에 대한 해결책이 있다면-특히 당신이 언급 한 것처럼 매우 간단하다면-나는 그것에 대해 알고 싶습니다.
Dreamspace President

@EJP : 다시 묻고 싶습니다. 1) 당신이 내 머리 앞에서 당근처럼 매달렸다 는 사소한 해결책이 무엇인지, 2) 소켓 해결책이 소켓 해결책 인 경우, 하나 이상의 저의 "소켓 접근 방식의 단점"글 머리 기호가 적용되며 3) 그렇다면 왜 그런 단점에도 불구하고 저와 같은 접근 방식보다 그 접근 방식을 권장합니다.
Dreamspace 대통령

@EJP : 문제는 당신의 목소리가 확실 알고있는 한, 꽤 무게를 가지고 있지만, 내가 가지고있는 모든 증거이다 힘이 나를 여기에 귀하의 조언이 잘못된 것을 확신 할 수 있습니다. 보세요, 저는 제 솔루션이 옳다고 주장하는 것이 아니라 증거 기반 기계입니다. 당신은 당신의 위치가 당신에게 제공한다는 표시되지 않는 책임이 있다고이 통신의 누락 된 퍼즐 조각을 채우기 위해 지역 사회를 당신이 시작?
Dreamspace 대통령

@EJP : 슬프게도 여러분의 반응이 없었기 때문에 여기에 사실로 가정하겠습니다. 서버 소켓 솔루션에 대한 진실은 실제로 는 심각한 결함 이 있으며이를 선택한 대부분의 이유는 "기타 이것도 사용하십시오. "또는 무책임한 사람들이 사용하도록 속일 수 있습니다. 나는 추측 당신이 필요한 설명으로 우리를 존엄하지 않은 이유의 일부를 당신이이 방법에 의문을 제기하지 않을 이유를 / 추측 할 수 없으며,이 계시 공개 성명을 발표하지 않을 것입니다.
Dreamspace President

1

Unique4j 라이브러리는 Java 애플리케이션의 단일 인스턴스를 실행하고 메시지를 전달하는 데 사용할 수 있습니다. https://github.com/prat-man/unique4j 에서 볼 수 있습니다 . Java 1.6 이상을 지원합니다.

파일 잠금 및 동적 포트 잠금의 조합을 사용하여 하나의 인스턴스 만 실행하도록 허용하는 기본 목표로 인스턴스 간을 감지하고 통신합니다.

다음은 동일한 간단한 예입니다.

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();

    }

}

면책 조항 : Unique4j 라이브러리를 만들고 유지합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.