Статьи

Напишите автоотладчик для отлова исключений во время выполнения теста

Ранее я заявлял, что есть некоторые исключения, для которых вы всегда хотели бы сохранить точку останова отладчика . Эта помощь предотвращает гниение кода без вашего ведома — иногда маскировка другой проблемы.

Если вы относитесь к этому серьезно, то стоит распространить эту идею на вас, автоматизированное тестирование; но придумать комплексное решение не совсем тривиально. Вы можете просто начать с 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

Как и всем, мне было бы интересно прочитать, было ли это чем-то полезным для людей и помочь устранить любые очевидные ошибки.