记得刚入行那会儿,我总觉得自己写的代码又快又好,直到某天团队来了个新同事,随手打开我的代码文件扫了一眼就说:"这里有个潜在的空指针风险"。当时我就懵了——这代码明明跑得好好的啊!后来才知道,原来他用了SonarLint这个神器。
SonarLint就像是个24小时在线的代码审查专家,它能实时检查你写的每一行代码。安装起来特别简单,在IDEA的插件市场搜索SonarLint,点安装然后重启IDE就搞定了。装好后你会发现编辑器里多了个小图标,按Ctrl+Shift+S就能扫描当前文件,右键点击项目目录还能批量扫描整个模块。
我第一次用的时候被吓到了——一个不到200行的小项目居然扫出十几个问题!有未使用的变量、多余的import、不规范的异常处理...最要命的是,有些问题我压根没意识到是问题。比如下面这个例子:
java复制// 反面教材
try {
doSomething();
} catch (Exception e) {
log.error("出错啦", e);
throw e;
}
SonarLint会告诉你:异常要么记录要么抛出,不要同时做两件事。因为这样会导致日志里出现重复的错误信息,让排查问题变得困难。正确的做法应该是:
java复制// 推荐做法
try {
doSomething();
} catch (SpecificException e) { // 具体异常而非泛型Exception
throw new BusinessException("业务处理失败", e);
}
先说几个新手最容易踩的坑。第一个是"局部变量不应该声明后立即返回",我见过不少同事这样写:
java复制// 不推荐
public String getName() {
String name = "张三";
return name;
}
// 推荐
public String getName() {
return "张三";
}
这种写法不仅多余,还会让代码显得很啰嗦。SonarLint会直接标黄提示,点击问题旁边的灯泡就能一键修复。
另一个常见问题是"不使用的私有字段应该被删除"。我们项目里就遇到过,某个类里有十几个字段,实际用到的不到一半。这些僵尸字段不仅占用内存,还会让读代码的人困惑。用SonarLint扫描后,它会列出所有未使用的字段,你可以放心删除。
最让我头疼的是"代码段不应该被注释掉"这条规则。很多人(包括我)习惯把暂时不用的代码注释掉"以备后用",结果这些注释越积越多,最后谁也分不清哪些是有用的。现在我们的原则是:不用的代码直接删,有Git怕什么?
在多线程环境下,有个错误特别隐蔽:"非原语字段不应该是volatile的"。看这个例子:
java复制// 危险写法
volatile List<String> cache = new ArrayList<>();
// 正确做法
AtomicReference<List<String>> cache = new AtomicReference<>(new ArrayList<>());
volatile只能保证引用本身可见,不能保证集合内部元素的线程安全。我曾经就因为这个问题导致线上数据错乱,排查了整整两天!
另一个坑是"实例方法不应该写入静态字段"。有次我们有个统计调用次数的功能这样实现:
java复制class Counter {
static int count;
void increment() {
count++; // SonarLint会标红
}
}
在多线程环境下这会出大问题。应该改成用AtomicInteger,或者更好的做法是使用专门的metrics库。
"子类字段不应覆盖父类字段"这条规则救过我一次。当时我继承了一个基类,不小心定义了个同名字段:
java复制class Parent {
String config = "default";
}
class Child extends Parent {
String config = "custom"; // SonarLint会警告
}
这样会导致极其隐蔽的bug——通过父类方法访问到的config和直接访问子类config值不一样!正确的做法是:
java复制class Child extends Parent {
Child() {
super.config = "custom"; // 显式修改父类字段
}
}
还有"实用程序类不应该有公共构造函数"这条,说的就是那些只包含静态方法的工具类。给它们加public构造方法纯属多余,还可能被人误用。应该这样:
java复制class StringUtils {
private StringUtils() {} // 私有构造
public static boolean isEmpty(String s) {...}
}
SonarLint有个特别有用的指标叫"认知复杂度",它会计算理解一个方法需要花费的脑力。我们团队规定超过15就要重构。来看个典型案例:
java复制// 复杂度25的糟糕方法
public void process(Order order) {
if (order != null) {
if (order.isValid()) {
for (Item item : order.getItems()) {
if (item.inStock()) {
if (item.isDiscount()) {
// 处理折扣逻辑
} else {
// 处理正常逻辑
}
}
}
}
}
}
// 重构后复杂度8
public void process(Order order) {
if (order == null || !order.isValid()) return;
order.getItems().stream()
.filter(Item::inStock)
.forEach(this::processItem);
}
private void processItem(Item item) {
if (item.isDiscount()) {
// 处理折扣逻辑
} else {
// 处理正常逻辑
}
}
通过提前返回和分解方法,代码变得清晰多了。现在每当看到复杂度警告,我们就会条件反射地思考如何简化。
Spring开发者要注意"通过注入的依赖调用事务方法"这条规则。有次我写了这样的代码:
java复制@Service
public class OrderService {
@Transactional
public void createOrder() {
validate();
// 其他逻辑
}
@Transactional
public void validate() {...}
}
看起来没问题对吧?但直接调用this.validate()会导致事务失效!因为Spring的事务是通过代理实现的。正确做法是:
java复制@Service
public class OrderService {
private final OrderService self;
@Autowired
public OrderService(OrderService orderService) {
this.self = orderService;
}
@Transactional
public void createOrder() {
self.validate(); // 通过注入的实例调用
}
}
我们团队现在把SonarLint集成到了CI流程中,配合SonarQube使用。开发阶段SonarLint实时提示,提交代码时SonarQube会再次检查。刚开始大家很不适应,但现在代码质量明显提升,线上bug减少了约40%。
有个小技巧:不是所有规则都适合每个项目。我们根据实际情况调整了规则集,比如对遗留系统放宽了认知复杂度限制。在IDEA的SonarLint设置里可以很方便地启用/禁用特定规则。
最让我欣慰的是看到团队成员的进步。有个 junior 刚开始每周要解决50+个问题,现在降到不到10个。他说:"现在写代码时会不自觉地避开那些坑,感觉像有个老师在旁边实时指导。"