You want to write a custom SonarQube rule to detect a company-specific anti-pattern: direct database calls from REST controllers instead of going through the service layer. How do you implement and deploy a custom SonarQube plugin?
Quick Answer
Create a SonarQube plugin using the SonarQube Plugin API (Java). Define a custom rule that uses the AST (Abstract Syntax Tree) visitor pattern to detect when classes annotated with @RestController contain injected fields of Repository/DataSource types. Package as a JAR, deploy to the extensions/plugins directory, and restart SonarQube.
Detailed Answer
Plugin Development
SonarQube plugins are Java applications packaged as JARs. They use the SonarQube Plugin API to register analyzers, rules, and UI extensions. For custom rules, you implement a 'Check' that visits the Abstract Syntax Tree (AST) of each source file. The visitor pattern lets you inspect classes, methods, annotations, and field declarations. When the anti-pattern is detected, you raise an issue at the specific code location.
Rule Implementation
For detecting database calls in controllers: create an AST visitor that tracks the current class context. When entering a class annotated with @RestController/@Controller, check all injected fields (constructor params or @Autowired fields). If any field type matches Repository, JdbcTemplate, DataSource, EntityManager, or DSLContext, raise an issue. This ensures architectural boundaries are enforced automatically.
Testing Custom Rules
SonarQube provides a testing framework where you write sample Java files (compliant and non-compliant) and assert that your rule raises issues at expected locations. Use JavaCheckVerifier.newVerifier() to validate rule behavior. This is critical before deployment because false positives in custom rules erode developer trust quickly.
Deployment and Lifecycle
Package the plugin with Maven (sonar-packaging-maven-plugin), copy the JAR to $SONAR_HOME/extensions/plugins/, restart SonarQube, and activate the new rule in your Quality Profile. For enterprises, version the plugin in your artifact repository (Nexus/Artifactory) and automate deployment via configuration management. Monitor rule effectiveness: if a rule generates too many false positives, fix or deactivate it promptly.
Code Example
// Custom SonarQube rule: Detect direct DB access from controllers
// src/main/java/com/acme/sonar/rules/NoDirectDbInControllerRule.java
@Rule(key = "AcmeArchS001")
public class NoDirectDbInControllerRule extends IssuableSubscriptionVisitor {
private static final Set<String> CONTROLLER_ANNOTATIONS = Set.of(
"org.springframework.web.bind.annotation.RestController",
"org.springframework.web.bind.annotation.Controller"
);
private static final Set<String> DB_TYPES = Set.of(
"JdbcTemplate", "NamedParameterJdbcTemplate", "DataSource",
"EntityManager", "SessionFactory", "DSLContext"
);
@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.CLASS);
}
@Override
public void visitNode(Tree tree) {
ClassTree classTree = (ClassTree) tree;
if (!isController(classTree)) return;
// Check constructor parameters and fields
for (Tree member : classTree.members()) {
if (member.is(Tree.Kind.VARIABLE)) {
VariableTree field = (VariableTree) member;
String typeName = field.type().symbolType().name();
if (DB_TYPES.contains(typeName) || typeName.endsWith("Repository")) {
reportIssue(field,
"Controllers must not directly access databases. " +
"Inject a @Service class instead. See: https://wiki.acme.com/arch/layering");
}
}
}
}
private boolean isController(ClassTree classTree) {
return classTree.modifiers().annotations().stream()
.anyMatch(a -> CONTROLLER_ANNOTATIONS.contains(
a.annotationType().symbolType().fullyQualifiedName()));
}
}
// Rule metadata: src/main/resources/org/sonar/l10n/java/rules/acme/AcmeArchS001.html
// <p>REST controllers should delegate data access to service-layer beans...</p>
// Rule test
// src/test/java/com/acme/sonar/rules/NoDirectDbInControllerRuleTest.java
@Test
void test_rule() {
JavaCheckVerifier.newVerifier()
.onFile("src/test/files/NonCompliantController.java")
.withCheck(new NoDirectDbInControllerRule())
.verifyIssues(); // Expects // Noncompliant comments in test file
}
// Test fixture: src/test/files/NonCompliantController.java
@RestController
public class OrderController {
private final OrderRepository orderRepo; // Noncompliant
private final JdbcTemplate jdbc; // Noncompliant
private final OrderService orderService; // Compliant
}
// pom.xml plugin packaging
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.23.0.740</version>
<extensions>true</extensions>
<configuration>
<pluginKey>acme-rules</pluginKey>
<pluginName>Acme Custom Rules</pluginName>
<pluginClass>com.acme.sonar.AcmeRulesPlugin</pluginClass>
</configuration>
</plugin>
# Deploy plugin
mvn clean package
cp target/acme-rules-1.0.jar $SONAR_HOME/extensions/plugins/
systemctl restart sonarqube
# Activate rule in profile
curl -u admin:$TOKEN -X POST \
"https://sonar.company.com/api/qualityprofiles/activate_rule" \
-d "key=acme-backend-java&rule=acme-rules:AcmeArchS001&severity=MAJOR"Interview Tip
Custom rules show advanced SonarQube knowledge. Emphasize the testing aspect - untested custom rules cause false positive floods that destroy developer trust. Mention the alternative: Semgrep for simpler pattern matching without the full plugin machinery. Position custom rules as an architectural governance tool.