头图

某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。

我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是用文档编辑器来写代码呢!!!

其实,打断点不丢脸,丢脸的是工作若干年后只知道最基础的断点调试!大明哥就见过有同事因为 for 循环里面实体对象报空指针异常,不知道怎么调试,选择一条一条得看,极其浪费时间!!所以,大明哥来分享一些 debug 技巧,赶紧收藏,日后好查阅!!

Debug 分类

对于很多同学来说,他们几乎就只知道在代码上面打断点,其实断点可以打在多个地方。

行断点

行断点的投标就是一个红色的圆形点。在需要断点的代码行头点击即可打上:

方法断点

方法断点就是将断点打在某个具体的方法上面,当方法执行的时候,就会进入断点。这个当我们阅读源码或者跟踪业务流程时比较有用。尤其是我们在阅读源码的时候,我们知道优秀的源码(不优秀的源码你也不会阅读)各种设计模式使用得飞起,什么策略、模板方法等等。具体要走到哪个具体得实现,还真不是猜出来,比如下面代码:

public interface Service {
    void test();
}

public class ServiceA implements Service{
    @Override
    public void test() {
        System.out.println("ServiceA");
    }
}


public class ServiceB implements Service{
    @Override
    public void test() {
        System.out.println("ServiceB");
    }
}


public class ServiceC implements Service{
    @Override
    public void test() {
        System.out.println("ServiceC");
    }
}


public class DebugTest {
    public static void main(String[] args) {
        Service service = new ServiceA();
        service.test();
    }
}

在运行时,你怎么知道他要进入哪个类的 test() 方法呢?有些小伙伴可能就会在 ServiceA、ServiceB、ServiceC 中都打断点(曾经我也是这么干的,初学者可以理解...),这样就可以知道进入哪个了。其实我们可以直接在接口 Service 的 test() 方法上面打断点,这样也是可以进入具体的实现类的方法:

当然,也可以在方法调用的地方打断点,进入这个断点后,按 F7 就可以了。

属性断点

我们也可以在某个属性字段上面打断点,这样就可以监听这个属性的读写变化过程。比如,我们定义这样的:

@Getter
@Setter
@AllArgsConstructor
public class Student {

    private String name;

    private Integer age;
}

public class ServiceA implements Service{
    @Override
    public void test() {
        Student student = new Student("张三",12);

        System.out.println(student.getName());

        student.setName("李四");
    }
}

图片

如下:

顺便吆喝一句,新机会

民族企业核心部门,base武汉、深圳、东莞、西安、上海、北京、苏州等地

前、后端or测试>>>直通路,  语言:Java、Js、测试、python、ios、安卓、C++(客户端)等

断点技巧

条件断点

在某些场景下,我们需要在特定的条件进入断点,尤其是 for 循环中(我曾经在公司看到一个小伙伴在循环内部看 debug 数据,惊呆我了),比如下面代码:

public class DebugTest {

    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        for (int i = 1 ; i < 1000 ; i++) {
            if (new Random().nextInt(100) % 10 == 0) {
                studentList.add(new Student("skjava-" + i, i));
            } else {
                studentList.add(new Student("damingge-" + i, i));
            }
        }

        for (Student student : studentList) {
            System.out.println(student.toString());
        }
    }
}

我们在 System.out.println(student.toString()); 打个断点,但是要 name 以 "skjava" 开头时才进入,这个时候我们就可以使用条件断点了:

条件断点是非常有用的一个断点技巧,对于我们调试复杂的业务场景,尤其是 for、if 代码块时,可以节省我们很多的调试时间。

模拟异常

这个技巧也是很有用,在开发阶段我们就需要人为制造异常场景来验证我们的异常处理逻辑是否正确。比如如下代码:

public class DebugTest {

    public static void main(String[] args) {
        methodA();
        
        try {
            methodB();
        } catch (Exception e) {
            e.printStackTrace();
            // do something
        }
        
        methodC();
    }
    
    public static void methodA() {
        System.out.println("methodA...");
    }

    public static void methodB() {
        System.out.println("methodA...");
    }

    public static void methodC() {
        System.out.println("methodA...");
    }
}

我们希望在 methodB() 方法中抛出异常,来验证 catch(Exception e) 中的 do something 是否处理正确。以前大明哥是直接在 methodB() 中 throw 一个异常,或者 1 / 0。这样做其实并没有什么错,只不过不是很优雅,同时也会有一个风险,就是可能会忘记删除这个测试代码,将异常提交上去了,最可怕的还是上了生产。

所以,我们可以使用 idea 模拟异常。

我们首先在 methodB() 打上一个断点
运行代码,进入断点处
在 Frames 中找到对应的断点记录,右键,选择 Throw Execption
输入你想抛出的异常,点击 ok 即可

这个技巧在我们调试异常场景时非常有用!!!

多线程调试

不知道有小伙伴遇到过这样的场景:在你和前端进行本地调试时,你同时又要调试自己写的代码,前端也要访问你的本地调试,这个时候你打断点了,前端是无法你本地的。为什么呢?因为 Idea 在 debug 时默认阻塞级别为 ALL,如果你进入 debug 场景了,idea 就会阻塞其他线程,只有当前调试线程完成后才会走其他线程。

这个时候,我们可以在 View Breakpoints 中选择 Thread,同时点击 Make Default设置为默认选项。这样,你就可以调试你的代码,前端又可以访问你的应用了。

或者

调试 Stream

Java 中的 Stream 好用是好用,但是依然有一些小伙伴不怎么使用它,最大的一个原因就是它不好调试。你利用 Stream 处理一个 List 对象后,发现结果不对,但是你很难判断到底是哪一行出来问题。我们看下面代码:

public class DebugTest {

    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        for (int i = 1; i < 1000; i++) {
            if (new Random().nextInt(100) % 10 == 0) {
                studentList.add(new Student("skjava-" + i, i));
            } else {
                studentList.add(new Student("damingge-" + i, i));
            }
        }

        studentList = studentList.stream()
                .filter(student -> student.getName().startsWith("skjava-"))
                .peek(item -> {
                    item.setName(item.getName() + "-**");
                    item.setAge(item.getAge() * 10);
                }).collect(Collectors.toList());
    }
}

在 stream() 打上断点,运行代码,进入断点后,我们只需要点击下图中的按钮:

在这个窗口中会记录这个 Stream 操作的每一个步骤,我们可以点击每个标签来看数据处理是否符合预期。这样是不是就非常方便了。

有些小伙伴的 idea 版本可能过低,需要安装 Java Stream Debugger 插件才能使用。

操作回退

我们 debug 调试的时候肯定不是一行一行代码的调试,而是在每个关注点处打断点,然后跳着看。但是跳到某个断点处时,突然发现有个变量的值你没有关注到需要回退到这个变量值的赋值处,这个时候怎么办?我们通常的做法是重新来一遍。虽然,可以达到我们的预期效果,但是会比较麻烦,其实 idea 有一个回退断点的功能,非常强大。在 idea 中有两种回退:

Reset Frame

看下面代码:

public class DebugTest {

    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = (a + b) * 2;

        int d = addProcessor(a, b,c);

        System.out.println();
    }

    private static int addProcessor(int a, int b, int c) {
        a = a++;
        b = b++;
        return a + b + c;
    }
}

我们在 addProcessor() 的 return a + b + c; 打上断点,到了这里 a 和 b 的值已经发生了改变,如果我们想要知道他们两的原始值,就只能回到开始的地方。idea 提供了一个 Reset Frame 功能,这个功能可以回到上一个方法处。

Jump To Line

Reset Frame 虽然可以用,但是它有一定的局限性,它只能方法级别回退,是没有办法向前或向后跳着我们想要执行的代码处。但 Jump To Line 可以做到。

Jump To Line 是一个插件,所以,需要先安装它。

由于大明哥使用的 idea 版本是 2024.2,这个插件貌似不支持,所以就在网上借鉴了一张图:

在执行到 debug 处时,会出现一个黄颜色的箭头,我们可以将这个箭头拖动到你想执行的代码处就可以了。向前、向后都可以,是不是非常方便。

目前这 5 个 debug 技巧是大明哥在工作中运用最多的,还有一个就是远程 debug 调试,但是这个我个人认为是野路子,大部分公司一般是不允许这么做的,所以就不演示了!

——转载自作者:大明哥_


幸福的闹钟
58 声望17 粉丝