It’s been a long time since I wrote a blog, so today we’re going to talk about Java program breakpoint debugging. The main reason for writing this topic is that companies or individuals around are all attached to developing APAAS platform. Simply speaking, APAAS platform is a zero-code or low-code configuration platform, through which web and mobile codes can be configured relatively quickly. When I was in 2015, I also made such a configuration platform with a front-end friend in order to facilitate rapid outsourcing. I worked for more than two years, and then I worked on apaAS platform in a company for more than one year. I can say THAT I have deep experience in this. First zero code is only suitable for children’s toys in the field of programming obviously, think zero code can pack all of them, wasn’t much of a personal guess either code, or being brainwashed, object-oriented if all systems in the world of business is simply to invoke methods through a variety of objects, and then through a certain logical combination, So does with zero code, but the specific object logic isn’t more complex process oriented code, cycle a few times, several layers of nested loops, loop in a variety of jump, pass a number of local variables, jump out of several layers of circulation and so on, so that how to use the zero code logic pictures come out? I think even if I could draw it, it would be a lot harder than just lifting the code. Compared to low code is more reliable, low code is to write code, if you want to write code without providing debugging, is that a rogue?

In fact, when I was in 2015, I also encountered the embarrassment of needing to provide debugging functions on the configuration platform. At that time, I searched online, but could not find the breakpoint debugging code that could be directly used. Therefore, I designed a poor explanatory language by myself, which can be found from the previous blog. Using AOP + method interceptor +stack to implement simple breakpoint debugging. However, the implementation has always felt very poor. Just now, I decided to study Java breakpoint debugging, and found the next batch of JDI implementations. But basically it’s all about a bunch of theories, and then a HelloWord example, and then a breakpoint, and then a breakpoint event, and then a breakpoint event, and then a breakpoint event, and then a log, and then nothing. I was thinking if you want to print logs using AOP isn’t it fragrant? What about debugging? Go one step further: debug multithreaded applications using JDI, click on mindless consumption events, and print a bunch of logs. I can’t think of a breakpoint debugging program that I can use directly on the Internet after so many years, but I also want to thank you, if you already have, then I don’t have anything to do with this article. In view of a pile of JDI theoretical articles on the Internet, in order to write only the original works that can not be found on the Internet, this article decided not to talk about the theory, there are a lot of theory baidu, search “JDI debugging”.

Java.tools.jar (JDK) has implemented a set of debug JDI API, also known as Java debug interface, but it is really a very time to use, in order to increase the interest of you to demonstrate the effect of breakpoint debugging, next to enter my favorite post code link, first prepare the following debug services

1 package com.rdpaas.debugger.test.controller; 2 3 import com.rdpaas.debugger.test.bean.Person; 4 import com.rdpaas.debugger.test.service.TestService; 5 import com.rdpaas.debugger.test.utils.MyList; 6 import com.rdpaas.debugger.test.utils.MyMap; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RequestParam; 10 import org.springframework.web.bind.annotation.RestController; 11 12 import java.util.Arrays; 17 @author rongdi 17 * @date 2021/1/24 18 */ 19 @restController 20 public class TestController { 21 22 @Autowired 23 private TestService testService; 24 25 private Integer flag = 1; 26 27 @RequestMapping("/test") 28 public Person test(@RequestParam String name) throws Exception { 29 Person ret = testService.getPerson(name); 30 MyList list1 = new MyList(); 31 list1. AddAll (Arrays. AsList (1, 2, 3)); 32 MyList list2 = new MyList(); 33 list2.add(new Person(" 三",20)); 34 MyMap map1 = new MyMap(); 35 map1. Put ("name"," xiao Ming "); 36 MyMap map2 = new MyMap(); Map2. Put ("person",new person ("person", 30)); 38 return ret; 39} 40 41 42}Copy the code
1 package com.rdpaas.debugger.test.service; 2 3 import com.rdpaas.debugger.test.bean.Person; 4 import org.springframework.stereotype.Service; 5 6 @Service 7 public class TestService { 8 9 public Person getPerson(String name) { 10 Person p = new Person(); 11 p.setAge(20); 12 p.setName(name); 13 return p; 14} 15 16}Copy the code
1 package com.rdpaas.debugger.test.bean; 2 3 public class Person { 4 5 private String name; 6 7 private Integer age; 8 9 public Person(String name, Integer age) { 10 this.name = name; 11 this.age = age; 12 } 13 14 public Person() { 15 } 16 17 public String getName() { 18 return name; 19 } 20 21 public void setName(String name) { 22 this.name = name; 23 } 24 25 public Integer getAge() { 26 return age; 27 } 28 29 public void setAge(Integer age) { 30 this.age = age; 31} 32}Copy the code
1 package com.rdpaas.debugger.test.utils; 2 3 import java.util.ArrayList; 7 public class MyList extends ArrayList {9 10} 7 public class MyList extends ArrayList {9 10}Copy the code
1 package com.rdpaas.debugger.test.utils; 2 3 import java.util.HashMap; Public class MyMap extends HashMap {9 10} public class MyMap extends HashMap {9 10}Copy the code
1 package com.rdpaas.debugger.test; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 6 /** 7 * @author rongdi 8 * @date 2021/1/24 9 */ 10 @SpringBootApplication 11 public class RunTestApplication { 12 13  public static void main(String[] args) { 14 SpringApplication.run(RunTestApplication.class,args); 16 15}}Copy the code
Let's start by using the debug interface to break line 29 of the TestController classCopy the code

The breakpoint interface blocks and requests the breakpoint service

The interface being debugged is blocked, and the breakpoint interface has returned

Finally, we finish debugging, and the debugged service returns with success

Rely on the following

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId>  </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>jdk.tools</groupId> < artifactId > JDK. Tools < / artifactId > < version > 1.8 < / version > < scope > system < / scope > <systemPath>${JAVA_HOME}\lib/tools.jar</systemPath> </dependency> </dependencies>Copy the code

Okay, so those of you who are interested should be able to stick with it. Remote debugging is different from native debugging of a JVM. JDI remote debugging returns only mirror objects, even the simplest generic data types, which makes it much more difficult to handle data, especially collections and maps, which we’ll see later.

First we need to prepare a program to be debugged, whether it is a Web service or a local program, but be sure to add the following startup parameters, in order to configure the various parameters needed for debugging

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5000
Copy the code

As shown above, a socket port 5000 is opened for remote debugging. The connection code is as follows

/** * Returns a virtual host object, * @param hostname Specifies the host address of the program to be debugged. * @param port Specifies the backdoor debugging port opened by the debugger. * @return * @throws Exception */ private VirtualMachine connJVM(String hostname, Integer port) throws Exception { VirtualMachineManager vmm = Bootstrap.virtualMachineManager(); List<AttachingConnector> connectors = vmm.attachingConnectors(); SocketAttachingConnector sac = null; for(AttachingConnector ac:connectors) { if(ac instanceof SocketAttachingConnector) { sac = (SocketAttachingConnector) ac; }} if(sac == null) {throw new Exception(" SocketAttachingConnector not found "); } Map<String, Connector.Argument> arguments = sac.defaultArguments(); arguments.get("hostname").setValue(hostname); arguments.get("port").setValue(String.valueOf(port)); return sac.attach(arguments); }Copy the code

Ok, I admit it’s easier to post the code and add a detailed comment to it. It feels awkward to post the code and explain it outside. Here’s the code for debugging:

/**
     * 当断点到最后一行后,调用断开连接结束调试
     */
    public DebugInfo disconnect() throws Exception {
        virtualMachine.dispose();
        map.remove(tag);
        return getInfo();
    }/**
     * 在指定类的指定行打上断点
     * @param className 类的全限定名
     * @param line 断点所在的有效行号(不要不讲武德打在空白行上)
     * @throws Exception
     */
    private void markBreakpoint(String className, Integer line) throws Exception {
        /**
         * 根据虚拟主机拿到一个事件请求管理器
         */
        EventRequestManager eventRequestManager = virtualMachine.eventRequestManager();
        /**
         * 主要是为了添加当前断点是把之前断点事删掉,
         */
        if(eventRequest != null) {
            eventRequestManager.deleteEventRequest(eventRequest);
        }
        /**
         * 根据调试类的全限定名,拿到一个调试类的远程引用类型,请注意这里是远程调试,在当前调试程序的jvm中不会
         * 装载有被调试类,所以这里只能是得到一个包装后的类型,至于为啥是个集合,是因为这个被调试类可能正在被多个
         * 线程调用
         */
        List<ReferenceType> rts = virtualMachine.classesByName(className);
        if(rts == null || rts.isEmpty()) {
            throw new Exception("无法获取有效的debug类");
        }

        /**
         * 不要说我不讲武德,正常的本地调试在多线程环境中也只能调试最先到达的那个线程的调用,所以这里也是直接
         * 获取第一个线程调用,同样只能可怜兮兮的获取到一个Class的包装类型,谁叫我们是远程调试呢
         */
        ClassType classType = (ClassType) rts.get(0);
        /**
         * 根据行获取位置对象,这里为啥又是个集合,好吧我承认忽悠不过去了,我也不明白,谁叫这JDI是人家设计的呢
         */
        List<Location> locations = classType.locationsOfLine(line);
        if(locations == null || locations.isEmpty()) {
            throw new Exception("无法获取有效的debug行");
        }
        /**
         * 一如既往的获取第一个位置信息
         */
        Location location = locations.get(0);

        /**
         * 创建一个断点并激活,这是公式代码,下面的EventRequest.SUSPEND_EVENT_THREAD表示断点执行过程阻塞当前线程,
         * SUSPEND_ALL 表示阻塞所有线程。实际上创建并激活的事件请求会被放在一个时间队列中
         */
        BreakpointRequest breakpointRequest = eventRequestManager.createBreakpointRequest(location);
        breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        breakpointRequest.enable();

        /**
         * 当前断点创建好了,赶紧释放被调试程序,让他有机会执行到当前断点,如果不放行就会一直卡在当前断点之前的其它断点,
         * 没机会到这里了,这里选择在这里放行而不是在执行完上一个断点后马上放行是因为我们的断点调试的断点请求并不是
         * 刚开始调试就确定好的,而是执行到当前行后由前端判断本行是否有断点,然后请求到调试程序的,属于动态添加断点,
         * 如果上一个断点执行完,马上释放那么当前断点可能都还没请求就过去了。
         */
        if(eventsSet != null) {
            eventsSet.resume();
        }

    }

    /**
     * 消费调试的事件请求,然后拿到当前执行的方法,参数,变量等信息,也就是debug过程中我们关注的那一堆变量信息
     * @return
     * @throws Exception
     */
    private DebugInfo getInfo() throws Exception {
        DebugInfo debugInfo = new DebugInfo();
        EventQueue eventQueue = virtualMachine.eventQueue();
        /**
         * 这个是阻塞方法,当有事件发出这里才可以remove拿到EventsSet
         */
        eventsSet= eventQueue.remove();
        EventIterator eventIterator = eventsSet.eventIterator();
        if(eventIterator.hasNext()) {
            Event event = eventIterator.next();
            /**
             * 一个debug程序能够debug肯定要有个断点,直接从断点事件这里拿到当前被调试程序当前的执行线程引用,
             * 这个引用是后面可以拿到信息的关键,所以保存在成员变量中,归属于当前的调试对象
             */
            if(event instanceof BreakpointEvent) {
                threadReference = ((BreakpointEvent) event).thread();
            } else if(event instanceof VMDisconnectEvent) {
                /**
                 * 这种事件是属于讲武德的判断方式,断点到最后一行之后调用virtualMachine.dispose()结束调试连接
                 */
                debugInfo.setEnd(true);
                return debugInfo;
            }
            try {
                /**
                 * 获取被调试类当前执行的栈帧,然后获取当前执行的位置
                 */
                StackFrame stackFrame = threadReference.frame(0);
                Location location = stackFrame.location();

                /**
                 * 无脑的封装返回对象
                 */
                debugInfo.setClassName(location.declaringType().name());
                debugInfo.setMethodName(location.method().name());
                debugInfo.setLineNumber(location.lineNumber());
                /**
                 * 封装成员变量
                 */
                ObjectReference or = stackFrame.thisObject();
                if(or != null) {
                    List<Field> fields = ((LocationImpl) location).declaringType().fields();
                    for(int i = 0;fields != null && i < fields.size();i++) {
                        Field field = fields.get(i);
                        Object val = parseValue(or.getValue(field),0);
                        DebugInfo.VarInfo varInfo = new DebugInfo.VarInfo(field.name(),field.typeName(),val);
                        debugInfo.getFields().add(varInfo);
                    }
                }
                /**
                 * 封装局部变量和参数,参数是方法传入的参数
                 */
                List<LocalVariable> varList = stackFrame.visibleVariables();
                for (LocalVariable localVariable : varList) {
                    /**
                     * 这地方使用threadReference.frame(0)而不是使用上面已经拿到的stackFrame,从代码上看是等价,
                     * 但是有个很坑的地方,如果使用stackFrame由于下面使用threadReference执行过invokeMethod会导致
                     * stackFrame的isValid为false,再次通过stackFrame.getValue就会报错,每次重新threadReference.frame(0)
                     * 就没有问题,由于看不到源码,个人推测threadReference.frame(0)这里会生成一份拷贝stackFrame,由于手动执行方法,
                     * 方法需要用到栈帧会导致执行完方法,这个拷贝的栈帧被销毁而变得不可用,而每次重新获取最上面得栈帧,就不会有问题
                     */
                    DebugInfo.VarInfo varInfo = new DebugInfo.VarInfo(localVariable.name(),localVariable.typeName(),parseValue(threadReference.frame(0).getValue(localVariable),0));
                    if(localVariable.isArgument()) {
                        debugInfo.getArgs().add(varInfo);
                    } else {
                        debugInfo.getVars().add(varInfo);
                    }
                }
            } catch(AbsentInformationException | VMDisconnectedException e1) {
                debugInfo.setEnd(true);
                return debugInfo;
            } catch(Exception e) {
                debugInfo.setEnd(true);
                return debugInfo;
            }

        }

        return debugInfo;
    }

    /**
     * 费劲的转换,一切都是因为调试类和被调试类不在一个JVM中,所以拿到的对象都只是一个包装类,拿不到源对象
     * @param value 待解析的值
     * @param depth 当前深度编号
     * @return
     * @throws Exception
     */
    private Object parseValue(Value value,int depth) throws Exception {
        if(value instanceof StringReference || value instanceof IntegerValue || value instanceof BooleanValue
                || value instanceof ByteValue || value instanceof CharValue || value instanceof ShortValue
                || value instanceof LongValue || value instanceof FloatValue || value instanceof DoubleValue) {
            return parseCommonValue(value);
        } else if(value instanceof ObjectReference) {
            int localDepth = depth;
            ObjectReference obj = (ObjectReference) value;
            String type = obj.referenceType().name();
            if("java.lang.Integer".equals(type) || "java.lang.Boolean".equals(type) || "java.lang.Float".equals(type)
                    || "java.lang.Double".equals(type) || "java.lang.Long".equals(type) || "java.lang.Byte".equals(type)
                    || "java.lang.Character".equals(type)) {
                Field f = obj.referenceType().fieldByName("value");
                return parseCommonValue(obj.getValue(f));
            } else if("java.util.Date".equals(type)) {
                Field field = obj.referenceType().fieldByName("fastTime");
                Date date = new Date(Long.parseLong("" + obj.getValue(field)));
                return date;
            } else if(value instanceof ArrayReference) {
                ArrayReference ar = (ArrayReference) value;
                List<Value> values = ar.getValues();
                List<Object> list = new ArrayList<>();
                for(int i = 0;i < values.size();i++) {
                    list.add(parseValue(values.get(i),depth));
                }
                return list;
                /**
                 * 个人感觉都已经有点不讲武德了,实在没有找到更优雅的方法了
                 */
            } else if(isCollection(obj)) {
                Method toArrayMethod = obj.referenceType().methodsByName("toArray").get(0);
                value = obj.invokeMethod(threadReference, toArrayMethod, Collections.emptyList(), 0);
                return parseValue(value,++localDepth);
            }  else if(isMap(obj)) {
                /**
                 * 这里是一个比较巧妙的利用递归方式,将map先转成集合,然后再调用本方法转成数组,然后就可以走到ArrayReference进行处理
                 */
                Method entrySetMethod = obj.referenceType().methodsByName("entrySet").get(0);
                value = obj.invokeMethod(threadReference, entrySetMethod, Collections.emptyList(), 0);
                return parseValue(value,++localDepth);
            } else {
                Map<String,Object> map = new HashMap<>();
                String className = obj.referenceType().name();
                map.put("class",className);
                /**
                 * 到了Object就不继续了
                 */
                if("java.lang.Object".equals(className)) {
                    return map;
                }
                List<Field> fields = obj.referenceType().allFields();
                for(int i = 0;fields != null && i < fields.size();i++) {
                    localDepth = depth;
                    /**
                     * 这里有个递归,万一被调试类不讲武德搞一个无限自循环的对象,比如Person类里有个成员变量p直接声明的时候
                     * 就new一个Person,这样这个Person对象的深度是无限的,为了防止内存溢出,限制深度不超过2,你要是不信邪,
                     * 你改成5试试,就本例的例子,执行到最后一行后,继续stepOver,可以给你返回上十万行数据,呵呵
                     */
                    if(localDepth < 2) {
                        Field f = fields.get(i);
                        map.put(f.name(), parseValue(obj.getValue(f), ++localDepth));
                    }
                }
                return map;
            }
        }
        return null;
    }

    /**
     * 万恶的穷举,真的是很恶心,如果不转直接放这个包装的Value出去变成json后就拿不到真实的value值,
     * 别看打印的时候可以打印,还好这些鬼东西是有规律的,调试的时候试出来了一个,其余都出来了
     * @param value
     * @return
     */
    private Object parseCommonValue(Value value) {
        if(value instanceof StringReference) {
            return ((StringReferenceImpl) value).value();
        } else if(value instanceof IntegerValue) {
            return ((IntegerValueImpl) value).value();
        } else if(value instanceof BooleanValue) {
            return ((BooleanValueImpl) value).value();
        } else if(value instanceof ByteValue) {
            return ((ByteValueImpl) value).value();
        } else if(value instanceof CharValue) {
            return ((CharValueImpl) value).value();
        } else if(value instanceof ShortValue) {
            return ((ShortValueImpl) value).value();
        } else if(value instanceof LongValue) {
            return ((LongValueImpl) value).value();
        } else if(value instanceof FloatValue) {
            return ((FloatValueImpl) value).value();
        } else if(value instanceof DoubleValue) {
            return ((DoubleValueImpl) value).value();
        } else {
            return null;
        }
    }

    /**
     * 判断是不是集合,经过了多轮的纠结,最开始尝试使用java.util开头,包含List的,如:
     * type.startsWith("java.util.") && ((type.indexOf("List") != -1) || (type.indexOf("Set") != -1))
     * 结果发现太片面,不讲武德都没法形容了,如果是List的实现类就没办法了,只能通过这种方式了,毕竟找了很多api找不到直接判断
     * 这个调试的镜像对象是否是集合的方法。请不要作死,明明不是集合,非要给自己的类定义一个toArray方法
     */
    private boolean isCollection(ObjectReference obj) throws ClassNotLoadedException {
        List<Method> toArrayMethods = obj.referenceType().methodsByName("toArray");
        boolean flag = false;
        for(int i = 0;i < toArrayMethods.size();i++) {
            Method toArrayMethod = toArrayMethods.get(i);
            flag = (toArrayMethod.argumentTypes().size() == 0);
            if(flag) {
                break;
            }
        }
        return flag;
    }

    /**
     * 判断是不是Map,经过了多轮的纠结,最开始尝试使用java.util开头,包含Map的,如:
     * (type.startsWith("java.util.") && (type.indexOf("Map") != -1) && !type.endsWith("$Node"))
     * 还是发现太片面,如果是Map的实现类就没办法了,只能通过这种判断是否有不带桉树的entrySet方法的方式了,你自己实现
     * 的类总不会明明不是一个map,你非要定义一个entrySet方法,这种作死的情况,我就不管了,毕竟找了很多api找不到
     * 直接判断这个调试的镜像对象是否是map的方法。
     */
    private boolean isMap(ObjectReference obj) throws ClassNotLoadedException {
        List<Method> toArrayMethods = obj.referenceType().methodsByName("entrySet");
        boolean flag = false;
        for(int i = 0;i < toArrayMethods.size();i++) {
            Method toArrayMethod = toArrayMethods.get(i);
            flag = (toArrayMethod.argumentTypes().size() == 0);
            if(flag) {
                break;
            }
        }
        return flag;
    }
Copy the code

The above code, in fact, all my thoughts and difficulties when WRITING this code are all in the comments. Personally, I feel that the API provided by JDI is really difficult to use, requiring a lot of patience to break points and try API after API, and because I did not find the source code of Tools. jar, it is more difficult to use.

Finally, the relevant dependencies are as follows

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId>  </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>jdk.tools</groupId> < artifactId > JDK. Tools < / artifactId > < version > 1.8 < / version > < scope > system < / scope > <systemPath>${JAVA_HOME}\lib/tools.jar</systemPath> </dependency> </dependencies>Copy the code

One class that does it all, that’s my favorite way to do it, and some people might disagree with that and have some fancy design patterns that are very deep in invocation, and I would say, if the JVM doesn’t have optimizations like method inlining, The vast majority of code on the market has no performance, or else why tail-recursive optimization optimizes recursion into a loop. Each call to a method involves new method stack, save the local variables, destroy method such as stack cumbersome process, so every method call comes at a price, as a developer, I’ve always thought unnecessary don’t optimize the structure, the straight is the easiest way to understand the structure of optimizing structure does not bring performance boost, optimization algorithm is, The code with the most extreme performance is often the simplest.

Of course you need to use some design patterns for extensibility, but you need to use them where you need to extend. Big projects, big tools, complex design at the beginning, what are you going to extend, and what are the scalability bottlenecks. Many open source projects often written in fact is not convenient to let others understand, literally a call to all have a double-digit call depth, this is a technique or deliberately increase technical barriers, if I write a method has been to call, call more than 20 method, are you willing to bite the bullet and see or decisive give up? Some of the big factory code is not good to look for a job, is really no courage to look. Some open source software is not because people don’t want to participate in it, but because your code is complicated enough and the technology is great enough, few people can understand it, hehe. The next blog continues to achieve breakpoint debugging single step debugging related functions, interested in the public can pay attention to the same name, convenient real-time push updates and access to the complete source code.