Ранее я заявлял, что есть некоторые исключения, для которых вы всегда хотели бы сохранить точку останова отладчика . Эта помощь предотвращает гниение кода без вашего ведома — иногда маскировка другой проблемы.
Если вы относитесь к этому серьезно, то стоит распространить эту идею на вас, автоматизированное тестирование; но придумать комплексное решение не совсем тривиально. Вы можете просто начать с try / catch, но это не будет фиксировать исключения в других потоках. Вы также можете сделать что-нибудь, используя АОП; но в зависимости от фреймворка вы не гарантированно поймаете все, также это означает, что вы тестируете с немного другим кодом, который будет беспокоить некоторых.
Несколько дней назад я наткнулся на эту запись в блоге о том, как написать свой собственный отладчик , и мне стало интересно, возможна ли отладка самого процесса Java. Оказывается, вы можете, и вот код, который я придумал в рамках этого небольшого мысленного эксперимента.
Первая часть класса просто содержит какой-то довольно хакерский код, позволяющий угадать, какой порт потребуется для подключения к той же виртуальной машине, основываясь на параметрах запуска. Может быть возможно использовать механизм Attach для запуска отладчика; но я не видел очевидного способа заставить его работать. Тогда есть только пара фабричных методов, которые принимают список исключений для поиска.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
package com.kingsfleet.debug;import com.sun.jdi.Bootstrap;import com.sun.jdi.ReferenceType;import com.sun.jdi.VirtualMachine;import com.sun.jdi.connect.AttachingConnector;import com.sun.jdi.connect.Connector;import com.sun.jdi.connect.IllegalConnectorArgumentsException;import com.sun.jdi.event.ClassPrepareEvent;import com.sun.jdi.event.Event;import com.sun.jdi.event.EventQueue;import com.sun.jdi.event.EventSet;import com.sun.jdi.event.ExceptionEvent;import com.sun.jdi.event.VMDeathEvent;import com.sun.jdi.event.VMDisconnectEvent;import com.sun.jdi.request.ClassPrepareRequest;import com.sun.jdi.request.EventRequest;import com.sun.jdi.request.ExceptionRequest;import java.io.IOException;import java.lang.management.ManagementFactory;import java.lang.management.RuntimeMXBean;import java.util.Collections;import java.util.HashSet;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;public class ExceptionDebugger implements AutoCloseable { public static int getDebuggerPort() { // Try to work out what port we need to connect to RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); List<String> inputArguments = runtime.getInputArguments(); int port = -1; boolean isjdwp = false; for (String next : inputArguments) { if (next.startsWith("-agentlib:jdwp=")) { isjdwp = true; String parameterString = next.substring("-agentlib:jdwp=".length()); String[] parameters = parameterString.split(","); for (String parameter : parameters) { if (parameter.startsWith("address")) { int portDelimeter = parameter.lastIndexOf(":"); if (portDelimeter != -1) { port = Integer.parseInt(parameter.substring(portDelimeter + 1)); } else { port = Integer.parseInt(parameter.split("=")[1]); } } } } } return port; } public static ExceptionDebugger connect(final String... exceptions) throws InterruptedException { return connect(getDebuggerPort(),exceptions); } public static ExceptionDebugger connect(final int port, final String... exceptions) throws InterruptedException { ExceptionDebugger ed = new ExceptionDebugger(port, exceptions); return ed; } |
Конструктор создает простой поток демона, который запускает соединение обратно к ВМ. Очень важно сделать это отдельным потоком, иначе, очевидно, виртуальная машина остановится, когда мы достигнем точки останова. Это хорошая идея, чтобы убедиться, что код в этом потоке не выбрасывает одно из исключений — на данный момент я просто надеюсь на лучшее.
Наконец, код просто поддерживает список запрещенных исключений, если у вас было немного больше времени, можно было бы сохранить трассировку стека, где произошло исключение.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// // Instance variables private final CountDownLatch startupLatch = new CountDownLatch(1); private final CountDownLatch shutdownLatch = new CountDownLatch(1); private final Set<String> set = Collections.synchronizedSet(new HashSet<String>()); private final int port; private final String exceptions[]; private Thread debugger; private volatile boolean shutdown = false; // // Object construction and methods // private ExceptionDebugger(final int port, final String... exceptions) throws InterruptedException { this.port = port; this.exceptions = exceptions; debugger = new Thread(new Runnable() { @Override public void run() { try { connect(); } catch (Exception ex) { ex.printStackTrace(); } } }, "Self debugging"); debugger.setDaemon(true); // Don't hold the VM open debugger.start(); // Make sure the debugger has connected if (!startupLatch.await(1, TimeUnit.MINUTES)) { throw new IllegalStateException("Didn't connect before timeout"); } } @Override public void close() throws InterruptedException { shutdown = true; // Somewhere in JDI the interrupt was being eaten, hence the volatile flag debugger.interrupt(); shutdownLatch.await(); } /** * @return A list of exceptions that were thrown */ public Set<String> getExceptionsViolated() { return new HashSet<String>(set); } /** * Clear the list of exceptions violated */ public void clearExceptionsViolated() { set.clear(); } |
Основной метод соединения — это довольно простой блок кода, который обеспечивает соединение и настраивает любые начальные точки останова.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// // Implementation details // private void connect() throws java.io.IOException { try { // Create a virtual machine connection VirtualMachine attach = connectToVM(); try { // Add prepare and any already loaded exception breakpoints createInitialBreakpoints(attach); // We can now allow the rest of the work to go on as we have created the breakpoints // we required startupLatch.countDown(); // Process the events processEvents(attach); } finally { // Disconnect the debugger attach.dispose(); // Give the debugger time to really disconnect // before we might reconnect, couldn't find another // way to do this try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } finally { // Notify watchers that we have shutdown shutdownLatch.countDown(); } } |
Подключение к себе — это всего лишь процесс поиска правильного соединительного разъема, в данном случае Socket, хотя я думаю, что вы могли бы использовать транспорт с общей памятью на некоторых платформах, если немного изменили код.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
private VirtualMachine connectToVM() throws java.io.IOException { List<AttachingConnector> attachingConnectors = Bootstrap.virtualMachineManager().attachingConnectors(); AttachingConnector ac = null; found: for (AttachingConnector next : attachingConnectors) { if (next.name().contains("SocketAttach")) { ac = next; break; } } Map<String, Connector.Argument> arguments = ac.defaultArguments(); arguments.get("hostname").setValue("localhost"); arguments.get("port").setValue(Integer.toString(port)); arguments.get("timeout").setValue("4000"); try { return ac.attach(arguments); } catch (IllegalConnectorArgumentsException e) { throw new IOException("Problem connecting to debugger",e); } } |
Когда вы подключаете отладчик, вы не знаете, были ли загружены интересующие вас исключения, поэтому вам нужно зарегистрировать точки останова как для точки подготовки классов, так и для уже загруженных.
Обратите внимание, что точка останова устанавливается с помощью политики только для разрыва одного потока — в противном случае по понятным причинам текущая виртуальная машина остановится, если поток отладчика также будет переведен в спящий режим.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
private void createInitialBreakpoints(VirtualMachine attach) { // Our first exception is for class loading for (String exception : exceptions) { ClassPrepareRequest cpr = attach.eventRequestManager().createClassPrepareRequest(); cpr.addClassFilter(exception); cpr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); cpr.setEnabled(true); } // Then we can check each in turn to see if it have already been loaded as we might // be late to the game, remember classes can be loaded more than once // for (String exception : exceptions) { List<ReferenceType> types = attach.classesByName(exception); for (ReferenceType type : types) { createExceptionRequest(attach, type); } } } private static void createExceptionRequest(VirtualMachine attach, ReferenceType refType) { ExceptionRequest er = attach.eventRequestManager().createExceptionRequest( refType, true, true); er.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); er.setEnabled(true); } |
Цикл обработки событий опрашивает экземпляры EventSet, которые содержат один или несколько экземпляров Event. Не все эти события относятся к запросу точки останова, поэтому вам следует позаботиться о том, чтобы не всегда вызывать возобновление для набора событий. Это связано с тем, что у вас может быть два набора событий подряд с кодом, вызывающим возобновление, прежде чем вы сможете прочитать второй. Это приводит к пропущенным точкам останова, когда код догоняет.
По какой-то причине JDI, по-видимому, использует флаг прерывания, поэтому логическое свойство останавливает цикл с помощью метода close из ранее.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
private void processEvents(VirtualMachine attach) { // Listen for events EventQueue eq = attach.eventQueue(); eventLoop: while (!Thread.interrupted() && !shutdown) { // Poll for event sets, with a short timeout so that we can // be interrupted if required EventSet eventSet = null; try { eventSet = eq.remove(500); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); continue eventLoop; } // Just loop again if we have no events if (eventSet == null) { continue eventLoop; } // boolean resume = false; for (Event event : eventSet) { EventRequest request = event.request(); if (request != null) { int eventPolicy = request.suspendPolicy(); resume |= eventPolicy != EventRequest.SUSPEND_NONE; } if (event instanceof VMDeathEvent || event instanceof VMDisconnectEvent) { // This should never happen as the VM will exit before this is called } else if (event instanceof ClassPrepareEvent) { // When an instance of the exception class is loaded attach an exception breakpoint ClassPrepareEvent cpe = (ClassPrepareEvent) event; ReferenceType refType = cpe.referenceType(); createExceptionRequest(attach, refType); } else if (event instanceof ExceptionEvent) { String name = ((ExceptionRequest)event.request()).exception().name(); set.add(name); } } // Dangerous to call resume always because not all event suspend the VM // and events happen asynchornously. if (resume) eventSet.resume(); } }} |
Таким образом, все, что остается, это простой тестовый пример, поскольку это JDK 7, а ExceptionDebugger — AutoCloseable, мы можем сделать это с помощью конструкции try-with-resources, как показано ниже. Очевидно, что если вы проводите автоматизированное тестирование, используйте приспособления для тестирования по вашему выбору.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class Target { public static void main(String[] args) throws InterruptedException { try (ExceptionDebugger ex = ExceptionDebugger.connect( NoClassDefFoundError.class.getName())) { doSomeWorkThatQuietlyThrowsAnException(); System.out.println(ex.getExceptionsViolated()); } System.exit(0); } private static void doSomeWorkThatQuietlyThrowsAnException() { // Check to see that break point gets fired try { Thread t = new Thread(new Runnable() { public void run() { try { throw new NoClassDefFoundError(); } catch (Throwable ex) { } } }); t.start(); t.join(); } catch (Throwable th) { // Eat this and don't tell anybody } }} |
Поэтому, если вы запустите этот класс со следующим параметром VM, обратите внимание на suspend = n, иначе код не запустится, вы обнаружите, что он может подключиться к себе и начать работать.
|
1
|
-agentlib:jdwp=transport=dt_socket,address=localhost:5656,server=y,suspend=n |
Это даст вам следующий вывод, обратите внимание на дополнительную строку отладки с ВМ:
|
1
2
3
|
Listening for transport dt_socket at address: 5656 java.lang.NoClassDefFoundError Listening for transport dt_socket at address: 5656 |
Как и всем, мне было бы интересно прочитать, было ли это чем-то полезным для людей и помочь устранить любые очевидные ошибки.