代码质量管理工具:SonarQube常见的问题及正确解决方案
代码质量管理工具:SonarQube常见的问题及正确解决方案
SonarQube 简介
Sonar 是一个用于代码质量管理的开放平台。通过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具。
与持续集成工具(例如 Hudson/Jenkins 等)不同,Sonar 并不是简单地把不同的代码检查工具结果(例如 FindBugs,PMD 等)直接显示在 Web 页面上,而是通过不同的插件对这些结果进行再加工处理,通过量化的方式度量代码质量的变化,从而可以方便地对不同规模和种类的工程进行代码质量管理。
在对其他工具的支持方面,Sonar 不仅提供了对 IDE 的支持,可以在 Eclipse 和 IntelliJ IDEA 这些工具里联机查看结果;同时 Sonar 还对大量的持续集成工具提供了接口支持,可以很方便地在持续集成中使用 Sonar。
此外,Sonar 的插件还可以对 Java 以外的其他编程语言提供支持,对国际化以及报告文档化也有良好的支持
1."@RequestMapping" 方法应为“ public”
将调用具有@Controller注释的类的@RequestMapping注释部分的方法(直接或间接通过元注释-Spring Boot的@RestController是一个很好的示例)来处理匹配的Web请求。 即使该方法是私有的,也会发生这种情况,因为Spring会通过反射调用此类方法,而不检查可见性。
因此,将敏感方法标记为私有似乎是控制如何调用此类代码的好方法。 不幸的是,并非所有的Spring框架都以这种方式忽略可见性。 例如,如果您试图通过将其标记为@Secured来控制对敏感,私有@RequestMapping方法的Web访问,则无论用户是否被授权访问它,它仍将被调用。 这是因为AOP代理不适用于非公开方法。
除了@RequestMapping之外,此规则还考虑了Spring Framework 4.3中引入的注释:@ GetMapping,@ PostMapping,@ PutMapping,@ DeleteMapping,@ PatchMapping。
2.默认软件包中不应使用“ @SpringBootApplication”和“ @ComponentScan”
@ComponentScan用于确定哪些Spring Bean在应用程序上下文中可用。 可以使用basePackageClasses或basePackages(或其别名值)参数来配置要扫描的软件包。 如果未配置任何参数,则@ComponentScan将仅考虑带有注释的类的程序包。 在属于默认包的类上使用@ComponentScan时,将扫描整个类路径。
这将减慢应用程序的启动速度,并且该应用程序可能无法启动BeanDefinitionStoreException,因为您最终扫描了Spring Framework软件包本身。
在以下情况下,此规则会引起问题:
@ ComponentScan,@ SpringBootApplication和@ServletComponentScan用于默认包的类
@ComponentScan已使用默认程序包显式配置
不兼容代码示例
import org.springframework.boot.SpringApplication;
@SpringBootApplication //不合规; RootBootApp在默认包中声明
public class RootBootApp {
...
}
@ComponentScan("")
public class Application {
...
}
兼容解决方案
package hello;
import org.springframework.boot.SpringApplication;
@SpringBootApplication //合规 RootBootApp属于“ hello”包
public class RootBootApp {
...
}
3.不应使用双重检查锁定
双重检查锁定是在输入同步块之前和之后检查延迟初始化对象的状态,以确定是否初始化该对象。
如果不对float或int以外的任何可变实例进行额外同步,则无法以独立于平台的方式可靠地工作。使用延迟初始化的双重检查锁定任何其他类型的原始或可变对象风险第二个线程使用未初始化或部分初始化成员第一个线程仍然是创建它时,程序崩溃。
有多种解决方法。最简单的方法是根本不使用双重检查锁定,而是同步整个方法。对于早期版本的JVM,出于性能原因,通常建议不要同步整个方法。但是,在新的JVM中,同步性能已大大提高,因此,现在这是首选的解决方案。如果您希望完全避免使用同步,则可以使用内部静态类来保存引用。内部静态类保证延迟加载。
不兼容代码示例
@NotThreadSafe //线程不安全
public class DoubleCheckedLocking {
private static Resource resource;
public static Resource getInstance() {
if (resource == null) {//2
synchronized (DoubleCheckedLocking.class) {
if (resource == null)//1
resource = new Resource(); //第一个线程还没创建完,只是分配了内存,指了引用,线程执行上面,会判断resource != null,最终导致程序崩溃
}
}
return resource;
}
static class Resource {
}
}
兼容解决方案
@ThreadSafe
public class SafeLazyInitialization {
private static Resource resource;
public static synchronized Resource getInstance() {
if (resource == null)
resource = new Resource();
return resource;
}
static class Resource {
}
}
@ThreadSafe
public class ResourceFactory {
private static class ResourceHolder {
public static Resource resource = new Resource(); // This will be lazily initialised
}
public static Resource getResource() {
return ResourceFactory.ResourceHolder.resource;
}
static class Resource {
}
}
class ResourceFactory {
private volatile Resource resource;
public Resource getResource() {
Resource localResource = resource;
if (localResource == null) {
synchronized (this) {
localResource = resource;
if (localResource == null) {
resource = localResource = new Resource();
}
}
}
return localResource;
}
static class Resource {
}
}
4.资源应该关闭
在使用后,需要关闭实现Closeable接口或其超级接口AutoCloseable的连接,流,文件和其他类。 此外,必须在finally块中进行关闭调用,否则异常可能使调用无法进行。 最好在类实现AutoCloseable时,应使用“ try-with-resources”模式创建资源并将其自动关闭。
无法正确关闭资源将导致资源泄漏,这可能首先导致应用程序崩溃,然后可能使应用程序崩溃。
不兼容代码示例
private void readTheFile() throws IOException {
Path path = Paths.get(this.fileName);
BufferedReader reader = Files.newBufferedReader(path, this.charset);
// ...
reader.close(); // 不合规
// ...
Files.lines("input.txt").forEach(System.out::println); // 不合规:需要关闭流
}
private void doSomething() {
OutputStream stream = null;
try {
for (String property : propertyList) {
stream = new FileOutputStream("myfile.txt"); // 不合规
// ...
}
} catch (Exception e) {
// ...
} finally {
stream.close(); //打开了多个流。 仅最后一个关闭。
}
}
兼容解决方案
private void readTheFile(String fileName) throws IOException {
Path path = Paths.get(fileName);
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
reader.readLine();
// ...
}
// ..
try (Stream<String> input = Files.lines("input.txt")) {
input.forEach(System.out::println);
}
}
private void doSomething() {
OutputStream stream = null;
try {
stream = new FileOutputStream("myfile.txt");
for (String property : propertyList) {
// ...
}
} catch (Exception e) {
// ...
} finally {
stream.close();
}
}
Java 7引入了try-with-resources语句,该语句隐式关闭Closeables。 在try-with-resources语句中打开的所有资源都被该规则忽略。
4.“Random”对象应重复使用
每次需要一个随机值时,创建一个新的Random对象都是效率低下的,并且可能生成取决于JDK的非随机数。 为了获得更好的效率和随机性,请创建一个随机数,然后存储并重新使用它。
Random()构造函数每次尝试为种子设置一个不同的值。 但是,不能保证种子将是随机的,甚至是均匀分布的。 一些JDK将当前时间用作种子,这使得生成的数字根本不是随机的。
该规则查找每次调用方法并将其分配给局部随机变量时都会创建新的Random的情况。
不兼容代码示例
public void doSomethingCommon() {
Random rand = new Random(); // 不合规; 每次调用都创建一个新实例
int rValue = rand.nextInt();
//...
兼容解决方案
private Random rand = SecureRandom.getInstanceStrong(); // SecureRandom优先于Random
public void doSomethingCommon() {
int rValue = this.rand.nextInt();
//...
5.不再使用时应清除“ ThreadLocal”变量
一旦保持线程不再存在,就应该对ThreadLocal变量进行垃圾回收。 当重新使用保持线程时,可能会发生内存泄漏,在使用线程池的应用程序服务器上就是这种情况。
为避免此类问题,建议始终使用remove()方法清除ThreadLocal变量,以删除ThreadLocal变量的当前线程值。
另外,调用set(null)删除值可能会在映射中保留对该指针的引用,这在某些情况下可能导致内存泄漏。 使用remove可以更安全地避免此问题。
不兼容代码示例
public class ThreadLocalUserSession implements UserSession {
private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>();
public UserSession get() {
UserSession session = DELEGATE.get();
if (session != null) {
return session;
}
throw new UnauthorizedException("User is not authenticated");
}
public void set(UserSession session) {
DELEGATE.set(session);
}
public void incorrectCleanup() {
DELEGATE.set(null); // //不合规
}
// some other methods without a call to DELEGATE.remove()
}
兼容解决方案
public class ThreadLocalUserSession implements UserSession {
private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>();
public UserSession get() {
UserSession session = DELEGATE.get();
if (session != null) {
return session;
}
throw new UnauthorizedException("User is not authenticated");
}
public void set(UserSession session) {
DELEGATE.set(session);
}
public void unload() {
DELEGATE.remove(); // 合规
}
// ...
}
6.字符串和包装类型应使用“equals()"进行比较
使用引用相等==或!=比较java.lang.String或包装类型(如java.lang.Integer)的两个实例几乎总是一个错误,因为它不是在比较实际值,而是在内存中的位置。
不兼容代码示例
String firstName = getFirstName(); // String overrides equals
String lastName = getLastName();
if (firstName == lastName) { ... }; //不合规; 即使字符串具有相同的值,也为false
兼容解决方案
String firstName = getFirstName();
String lastName = getLastName();
if (firstName != null && firstName.equals(lastName)) { ... };
在Java 中包装类型与基本数据类型存储位置不同。
Java 基本数据类型存放位置
- 方法参数、局部变量存放在栈内存中的栈桢中的局部变量表
- 常量存放在常量池中
包装类型如Integer存放位置
- 常量池
- 堆内存
Integer 存储在常量池中时可以使用对比,但当在堆内存中时,使用对比,实际对比的是两个内存地址而非值。
根据Integer源码,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUMu95xG-1588123019546)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1583733976376.png)]
可以看出数值在-128-127时,会使用cache中的数据,其实也就是常量池。超过范围后新创建Integer,此时数据就无法使用==。
本项规则,主要就是为了避免对比内存地址而引发的错误判断。
7.“ compareTo”不应重载
在实现Comparable .compareTo方法时,参数的类型必须与Comparable声明中使用的类型匹配。 当使用其他类型时,这将创建一个重载而不是一个重写,这不太可能成为意图。
当实现Comparable 的类的compareTo方法的参数与Comparable声明中使用的参数不同时,此规则会引起问题。
不兼容代码示例
public class Foo {
static class Bar implements Comparable<Bar> {
public int compareTo(Bar rhs) {
return -1;
}
}
static class FooBar extends Bar {
public int compareTo(FooBar rhs) { //不合规参数类型必须为Bar
return 0;
}
}
}
兼容解决方案
public class Foo {
static class Bar implements Comparable<Bar> {
public int compareTo(Bar rhs) {
return -1;
}
}
static class FooBar extends Bar {
public int compareTo(Bar rhs) {
return 0;
}
}
}
8.周年(“YYYY”)不应用于日期格式
当使用SimpleDateFormat格式化和解析日期时,很少有开发人员会意识到“周年”的Y和“年”的y之间的区别。 这很可能是因为对于大多数日期而言,“周年”和“年”是相同的,因此在除该年的第一周或最后一周之外的任何时间进行测试,都会得到y和Y相同的值。但是在12月的最后一周和 一月的第一周,您可能会得到意想不到的结果。
不兼容代码示例
Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31");
String result = new SimpleDateFormat("YYYY/MM/dd").format(date); //Noncompliant; yields '2016/12/31'
兼容解决方案
Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31");
String result = new SimpleDateFormat("yyyy/MM/dd").format(date); //Yields '2015/12/31' as expected
异常
Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31");
String result = new SimpleDateFormat("YYYY-ww").format(date); //compliant, 'Week year' is used along with 'W
9.装箱和拆箱不应连续操作
装箱是将原始值放入类似对象的过程,例如创建一个Integer来保存一个int值。 拆箱是从此类对象中检索原始值的过程。
由于在装箱和拆箱期间原始值保持不变,因此在不需要时进行任何操作都是没有意义的。 这也适用于自动装箱和自动拆箱(当Java为您隐式处理原始/对象转换时)。
不兼容代码示例
public void examineInt(int a) {
//...
}
public void examineInteger(Integer a) {
// ...
}
public void func() {
int i = 0;
Integer iger1 = Integer.valueOf(0);
double d = 1.0;
int dIntValue = new Double(d).intValue(); // Noncompliant
examineInt(new Integer(i).intValue()); // Noncompliant; explicit box/unbox
examineInt(Integer.valueOf(i)); // Noncompliant; boxed int will be auto-unboxed
examineInteger(i); // Compliant; value is boxed but not then unboxed
examineInteger(iger1.intValue()); // Noncompliant; unboxed int will be autoboxed
Integer iger2 = new Integer(iger1); // Noncompliant; unnecessary unboxing, value can be reused
}
兼容解决方案
public void examineInt(int a) {
//...
}
public void examineInteger(Integer a) {
// ...
}
public void func() {
int i = 0;
Integer iger1 = Integer.valueOf(0);
double d = 1.0;
int dIntValue = (int) d;
examineInt(i);
examineInteger(i);
examineInteger(iger1);
}
10.Boxed “Boolean” should be avoided in boolean expressions
在布尔表达式中应避免使用装箱的“布尔”
如果将装箱的类型java.lang.Boolean用作表达式,则如Java语言规范§5.1.8取消装箱转换中所定义的,如果该值为null,则它将抛出NullPointerException。
完全避免这种转换并显式处理null值是更安全的。
不兼容代码示例
Boolean b = getBoolean();
if (b) { // Noncompliant, 当b为null,回抛NPE
foo();
} else {
bar();
}
兼容解决方案
Boolean b = getBoolean();
if (Boolean.TRUE.equals(b)) { //注意这块写法
foo();
} else {
bar(); // will be invoked for both b == false and b == null
}
11.Math should not be performed on floats
对于较小的数字,浮点数学具有足够的精度来产生期望值,但对于较大的数字则不能。 BigDecimal是最好的选择,但是如果需要基元,则使用double。
不兼容代码示例
float a = 16777216.0f;
float b = 1.0f;
float c = a + b; // Noncompliant; yields 1.6777216E7 not 1.6777217E7
double d = a + b; // Noncompliant; addition is still between 2 floats
兼容解决方案
float a = 16777216.0f;
float b = 1.0f;
BigDecimal c = BigDecimal.valueOf(a).add(BigDecimal.valueOf(b));
double d = (double)a + (double)b;
12.Non-thread-safe fields should not be static
非线程安全的属性不能设置为静态
不兼容代码示例
public class MyClass {
private static SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss"); // Noncompliant
private static Calendar calendar = Calendar.getInstance(); // Noncompliant
兼容解决方案
public class MyClass {
private SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss");
private Calendar calendar = Calendar.getInstance();
线程不安全的类型设置为静态后,对于静态变量来说,类在加载的时候会占用同一个存储区,而每个线程都是公用这个存储区的,因此存在线程安全的问题。
在多并发的过程中容易产生问题,而且问题原因不易跟踪。
13.“ java.time”类应用于日期和时间
年代久远的Date和Calendar类一直令人困惑,难以正确使用,尤其是在多线程环境中。 长期以来,JodaTime一直是一种流行的选择,但现在内置了一个更好的选择。 Java 8的JSR 310实现为以下提供了特定的类:
Class | Use for |
---|---|
LocalDate | a date, without time of day, offset, or zone |
LocalTime | the time of day, without date, offset, or zone |
LocalDateTime | the date and time, without offset, or zone |
OffsetDate | a date with an offset such as +02:00, without time of day, or zone |
OffsetTime | the time of day with an offset such as +02:00, without date, or zone |
OffsetDateTime | the date and time with an offset such as +02:00, without a zone |
ZonedDateTime | the date and time with a time zone and offset |
YearMonth | a year and month |
MonthDay | month and day |
Year/MonthOfDay/DayOfWeek/… | classes for the important fields |
DateTimeFields | stores a map of field-value pairs which may be invalid |
Calendrical | access to the low-level API |
Period | a descriptive amount of time, such as “2 months and 3 days” |
不兼容代码示例
Date now = new Date(); // Noncompliant
DateFormat df = new SimpleDateFormat("dd.MM.yyyy");
Calendar christmas = Calendar.getInstance(); // Noncompliant
christmas.setTime(df.parse("25.12.2020"));
兼容解决方案
LocalDate now = LocalDate.now(); // 获取日历日期。 没有时间成分
LocalTime now2 = LocalTime.now(); // 获取当前时间。 没有日期成分
LocalDate christmas = LocalDate.of(2020,12,25);
14.“static” members should be accessed statically(“静态”成员应静态访问)
尽管可以从类实例访问静态成员,但是这种形式很差,并且大多数人认为这具有误导性,因为它向您的代码读者暗示每个类实例都有一个成员实例。
不兼容代码示例
public class A {
public static int counter = 0;
}
public class B {
private A first = new A();
private A second = new A();
public void runUpTheCount() {
first.counter ++; // Noncompliant
second.counter ++; // Noncompliant. A.counter is now 2, which is perhaps contrary to expectations
}
}
兼容解决方案
public class A {
public static int counter = 0;
}
public class B {
private A first = new A();
private A second = new A();
public void runUpTheCount() {
A.counter ++; // Compliant
A.counter ++; // Compliant
}
}
15.“InterruptedException” should not be ignored
绝不应该在代码中忽略InterruptedExceptions,在这种情况下,只需将异常计数记录为“忽略”即可。抛出InterruptedException会清除Thread的中断状态,因此,如果未正确处理该异常,则该线程被中断的事实将丢失。相反,应该立即或在清除方法状态后重新抛出InterruptedExceptions-或应该通过调用Thread.interrupt()重新中断线程,即使这应该是单线程应用程序也是如此。任何其他措施可能会导致线程关闭延迟,并丢失该线程被中断的信息-可能未完成其任务。
不兼容代码示例
public void run () {
try {
while (true) {
// do stuff
}
}catch (InterruptedException e) { // Noncompliant; logging is not enough
LOGGER.log(Level.WARN, "Interrupted!", e);
}
}
兼容解决方案
public void run () {
try {
while (true) {
// do stuff
}
}catch (InterruptedException e) {
LOGGER.log(Level.WARN, "Interrupted!", e);
// Restore interrupted state...
Thread.currentThread().interrupt();
}
}
16.“ getClass”不应用于同步
getClass不应该用于非final类的同步,因为子类将在不同于父类或其他类的对象上进行同步,从而允许多个线程同时进入代码块,尽管synchronized关键字是这样的。
相反,硬编码要同步或使类成为final的类的名称。
不兼容代码示例
public class MyClass {
public void doSomethingSynchronized(){
synchronized (this.getClass()) { //不合规
// ...
}
}
兼容解决方案
public class MyClass {
public void doSomethingSynchronized(){
synchronized (MyClass.class) { //合规
// ...
}
}
17.Constructor injection should be used instead of field injection(应该使用构造函数注入而不是字段注入)
字段注入似乎是一种让类完成它们的工作所需的整洁方法,但它实际上是一个等待发生的NullPointerException,除非所有的类构造函数都是私有的。这是因为由调用者构造的任何类实例,而不是由符合JSR-330 (Spring, Guice,…)的依赖注入框架实例化的,将不能执行字段注入。
相反,应该将@Inject移动到构造函数中,并将所需的字段作为构造函数参数。
当具有非私有构造函数(包括默认构造函数)的类使用字段注入时,此规则会引发问题。
不兼容代码示例
class MyComponent { // 任何人都可以调用默认构造函数
@Inject MyCollaborator collaborator; // Noncompliant
public void myBusinessMethod() {
collaborator.doSomething(); // 这将在调用者新创建的类中失败
}
}
兼容解决方案
class MyComponent {
private final MyCollaborator collaborator;
@Inject
public MyComponent(MyCollaborator collaborator) {
Assert.notNull(collaborator, "MyCollaborator must not be null!");
this.collaborator = collaborator;
}
public void myBusinessMethod() {
collaborator.doSomething();
}
}
18.“ volatile”变量不应与复合运算符一起使用
在原始字段上使用复合运算符以及递增和递减(在布尔值的情况下进行切换)不是原子操作。 也就是说,它们不会一步一步发生。 例如,当易失性原语字段递增或递减时,如果线程在更新步骤中交织,则存在数据丢失的风险。 而是使用保证原子的类(例如AtomicInteger)或同步访问。volatile只能保证可见性,不行保证原子性
不兼容代码示例
private volatile int count = 0;
private volatile boolean boo = false;
public void incrementCount() {
count++; // Noncompliant
}
public void toggleBoo(){
boo = !boo; // Noncompliant
}
兼容解决方案
private AtomicInteger count = 0;
private boolean boo = false;
public void incrementCount() {
count.incrementAndGet();
}
public synchronized void toggleBoo() {
boo = !boo;
}
19.".equals()" should not be used to test the values of “Atomic” classes
AtomicInteger和AtomicLong扩展Number,但它们与Integer和Long不同,因此应以不同的方式处理。 AtomicInteger和AtomicLong旨在支持对单个变量进行无锁的线程安全编程。 这样,AtomicInteger将永远只与自身“相等”。 相反,您应该.get()值并对其进行比较。
这适用于所有原子的,看似原始的包装器类:AtomicInteger,AtomicLong和AtomicBoolean。
不兼容代码示例
AtomicInteger aInt1 = new AtomicInteger(0);
AtomicInteger aInt2 = new AtomicInteger(0);
if (aInt1.equals(aInt2)) { ... } // Noncompliant
兼容解决方案
AtomicInteger aInt1 = new AtomicInteger(0);
AtomicInteger aInt2 = new AtomicInteger(0);
if (aInt1.get() == aInt2.get()) { ... }
20."toArray"应传递适当类型的数组
如果没有给出任何参数,Collections.toArray方法将返回一个Object [],如果您尝试将其强制转换为适当类的数组,则它将在运行时导致ClassCastException。 而是将正确类型的数组传递给调用。
不兼容代码示例
public String [] getStringArray(List<String> strings) {
return (String []) strings.toArray(); // Noncompliant; ClassCastException thrown
}
兼容解决方案
public String [] getStringArray(List<String> strings) {
return strings.toArray(new String[0]);
}
21.JEE applications should not “getClassLoader”
使用标准的getClassLoader()可能无法在JEE上下文中返回正确的类加载器。 相反,请通过currentThread。
不兼容代码示例
ClassLoader cl = this.getClass().getClassLoader(); // Noncompliant
兼容解决方案
ClassLoader cl = Thread.currentThread().getContextClassLoader();
22.“ StringBuilder”和“ StringBuffer”不应使用字符实例化
用字符实例化StringBuilder或StringBuffer会产生误导,因为大多数Java开发人员都希望字符是StringBuffer的初始值。
实际发生的是,字符的int表示用于确定StringBuffer的初始大小。
不兼容代码示例
StringBuffer foo = new StringBuffer('x'); //相当于StringBuffer foo = new StringBuffer(120);
兼容解决方案
StringBuffer foo = new StringBuffer("x");
23.Blocks should be synchronized on “private final” fields
对类字段进行同步不是对字段本身进行同步,而是对分配给它的对象进行同步。因此,在非final字段上进行同步可以使字段的值在线程处于与旧值同步的块中时发生更改。这将允许在新值上同步的第二个线程同时进入该块。
在参数同步方面,情况非常类似;两个并行运行该方法的不同线程可以将两个不同的对象实例作为参数传递给该方法,这完全破坏了同步。
不兼容代码示例
private String color = "red";
private void doSomething(){
synchronized(color) { // 不合规; 锁实际上在由颜色变量引用的对象实例“红色”上
//...
color = "green"; // 现在允许其他线程进入该块
// ...
}
synchronized(new Object()) { // 不合规,这是禁止操作。
// ...
}
}
兼容解决方案
private String color = "red";
private final Object lockObj = new Object();//重点
private void doSomething(){
synchronized(lockObj) {
//...
color = "green";
// ...
}
}
总结
SonarQube 进行代码质量检查,不仅可以分析当前代码已存在问题。也可以通过问题进行分析,把错误的代码习惯,改正。
长期使用SonarQube,可以培养开发者写优秀代码。降低bug率
微信公众号