Skip to content

Commit a89e604

Browse files
committed
feat: Add Support for Meta-Spring Controller Annotations
1 parent d0f9fc7 commit a89e604

17 files changed

+435
-14
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.itangcent.idea.plugin.api.export.spring
2+
3+
import com.google.inject.Inject
4+
import com.intellij.psi.PsiClass
5+
import com.itangcent.intellij.jvm.PsiResolver
6+
import java.util.concurrent.ConcurrentHashMap
7+
8+
/*
9+
* This class provides a custom implementation for resolving whether a given PsiClass
10+
* has a Spring controller annotation. It supports meta-annotations, allowing for
11+
* custom annotations that are themselves annotated with Spring controller annotations.
12+
* For example, a custom annotation that is annotated with @Controller will be recognized
13+
* ```java
14+
* @Target({ElementType.TYPE})
15+
* @Retention(RetentionPolicy.RUNTIME)
16+
* @Documented
17+
* @Controller
18+
* public @interface XxxxController {
19+
* @AliasFor(
20+
* annotation = Controller.class
21+
* )
22+
* String value() default "";
23+
* }
24+
* ```
25+
*/
26+
class CustomSpringControllerAnnotationResolver : SpringControllerAnnotationResolver {
27+
28+
@Inject
29+
private lateinit var psiResolver: PsiResolver
30+
31+
@Inject
32+
private lateinit var standardSpringControllerAnnotationResolver: StandardSpringControllerAnnotationResolver
33+
34+
/**
35+
* A cache to store the resolution results of whether a given annotation is a Spring controller annotation.
36+
* The key is the qualified name of the annotation, and the value is a boolean indicating if it is a controller annotation.
37+
*/
38+
private val controllerAnnotationLookup = ConcurrentHashMap<String, Boolean>()
39+
40+
override fun hasControllerAnnotation(psiClass: PsiClass): Boolean {
41+
return psiClass.annotations.any { annotation ->
42+
val annotationQualifiedName = annotation.qualifiedName ?: return@any false
43+
controllerAnnotationLookup.computeIfAbsent(annotationQualifiedName) {
44+
val annotationClass = annotation.resolveAnnotationType()
45+
?: psiResolver.resolveClass(annotationQualifiedName, psiClass)
46+
?: return@computeIfAbsent false
47+
return@computeIfAbsent standardSpringControllerAnnotationResolver.hasControllerAnnotation(
48+
annotationClass
49+
)
50+
}
51+
}
52+
}
53+
}

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/CustomSpringRequestMappingResolver.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@ import com.itangcent.intellij.context.ActionContext
1212
import com.itangcent.intellij.jvm.AnnotationHelper
1313
import java.util.concurrent.ConcurrentHashMap
1414

15-
/**
16-
* Support custom annotation which annotated with [SpringClassName.SPRING_REQUEST_MAPPING_ANNOTATIONS]
15+
/*
16+
* This class provides support for resolving custom request mapping annotations that are
17+
* themselves annotated with standard Spring request mapping annotations.
18+
*
19+
* It enables the use of custom annotations as alternatives to the standard Spring
20+
* request mapping annotations. For example:
21+
*
22+
* ```java
23+
* @Target({ElementType.METHOD})
24+
* @Retention(RetentionPolicy.RUNTIME)
25+
* @RequestMapping(method = RequestMethod.GET)
26+
* public @interface CustomGet {
27+
* String value() default "";
28+
* }
29+
* ```
30+
*
31+
* The resolver will recognize such custom annotations and extract the appropriate
32+
* mapping information from them by analyzing their meta-annotations.
1733
*/
1834
@Singleton
1935
class CustomSpringRequestMappingResolver : SpringRequestMappingResolver {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.itangcent.idea.plugin.api.export.spring
2+
3+
import com.intellij.psi.PsiClass
4+
import com.itangcent.spi.SpiCompositeLoader
5+
6+
/*
7+
* This class provides a default implementation for resolving whether a given PsiClass
8+
* has a Spring controller annotation. It delegates the resolution to a composite
9+
* loader that can handle multiple strategies.
10+
*/
11+
class DefaultSpringControllerAnnotationResolver : SpringControllerAnnotationResolver {
12+
13+
private val delegate: SpringControllerAnnotationResolver by lazy {
14+
SpiCompositeLoader.loadComposite()
15+
}
16+
17+
override fun hasControllerAnnotation(psiClass: PsiClass): Boolean {
18+
return delegate.hasControllerAnnotation(psiClass)
19+
}
20+
}

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/DefaultSpringRequestMappingResolver.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import com.intellij.psi.PsiElement
77
import com.itangcent.intellij.context.ActionContext
88
import com.itangcent.spi.SpiCompositeLoader
99

10+
/*
11+
* This class provides a default implementation of SpringRequestMappingResolver that uses
12+
* a composite pattern to delegate the resolution to multiple resolvers.
13+
*/
1014
@Singleton
1115
class DefaultSpringRequestMappingResolver : SpringRequestMappingResolver {
1216

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SimpleSpringRequestClassExporter.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.intellij.psi.PsiElement
77
import com.intellij.psi.PsiMethod
88
import com.itangcent.common.logger.traceError
99
import com.itangcent.common.model.Request
10-
import com.itangcent.common.utils.stream
1110
import com.itangcent.idea.condition.annotation.ConditionOnClass
1211
import com.itangcent.idea.plugin.api.ClassApiExporterHelper
1312
import com.itangcent.idea.plugin.api.export.condition.ConditionOnDoc
@@ -45,6 +44,9 @@ open class SimpleSpringRequestClassExporter : ClassExporter {
4544
@Inject
4645
protected lateinit var classApiExporterHelper: ClassApiExporterHelper
4746

47+
@Inject
48+
private lateinit var springControllerAnnotationResolver: SpringControllerAnnotationResolver
49+
4850
override fun support(docType: KClass<*>): Boolean {
4951
return docType == Request::class
5052
}
@@ -72,10 +74,12 @@ open class SimpleSpringRequestClassExporter : ClassExporter {
7274

7375
return false
7476
}
77+
7578
shouldIgnore(cls) -> {
7679
logger!!.info("ignore class: $clsQualifiedName")
7780
return true
7881
}
82+
7983
else -> {
8084
logger!!.info("search api from: $clsQualifiedName")
8185

@@ -92,9 +96,8 @@ open class SimpleSpringRequestClassExporter : ClassExporter {
9296
}
9397

9498
protected open fun isCtrl(psiClass: PsiClass): Boolean {
95-
return SpringClassName.SPRING_CONTROLLER_ANNOTATION.any {
96-
annotationHelper!!.hasAnn(psiClass, it)
97-
} || (ruleComputer.computer(ClassExportRuleKeys.IS_SPRING_CTRL, psiClass) ?: false)
99+
return springControllerAnnotationResolver.hasControllerAnnotation(psiClass)
100+
|| (ruleComputer.computer(ClassExportRuleKeys.IS_SPRING_CTRL, psiClass) ?: false)
98101
}
99102

100103
private fun shouldIgnore(psiElement: PsiElement): Boolean {

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringClassName.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.itangcent.idea.plugin.api.export.spring
22

3-
3+
/*
4+
* This object holds constants for various Spring class and annotation names.
5+
* It is used throughout the codebase to refer to these classes and annotations
6+
* in a consistent manner.
7+
*/
48
object SpringClassName {
59

610
val SPRING_REQUEST_RESPONSE: Array<String> = arrayOf(
@@ -58,8 +62,10 @@ object SpringClassName {
5862
//Spring Boot Actuator Annotations
5963
const val ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.Endpoint"
6064
const val WEB_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint"
61-
const val CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint"
62-
const val REST_CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint"
65+
const val CONTROLLER_ENDPOINT_ANNOTATION =
66+
"org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint"
67+
const val REST_CONTROLLER_ENDPOINT_ANNOTATION =
68+
"org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint"
6369

6470
const val READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.ReadOperation"
6571
const val WRITE_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.WriteOperation"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.itangcent.idea.plugin.api.export.spring
2+
3+
import com.google.inject.ImplementedBy
4+
import com.intellij.psi.PsiClass
5+
6+
/*
7+
* This interface defines a contract for resolving whether a given PsiClass
8+
* has a Spring controller annotation. It is implemented by various classes
9+
* to provide different strategies for determining the presence of controller annotations.
10+
*/
11+
@ImplementedBy(DefaultSpringControllerAnnotationResolver::class)
12+
interface SpringControllerAnnotationResolver {
13+
fun hasControllerAnnotation(psiClass: PsiClass): Boolean
14+
}

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringRequestClassExporter.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ open class SpringRequestClassExporter : RequestClassExporter() {
3434
@Inject
3535
protected lateinit var springRequestMappingResolver: SpringRequestMappingResolver
3636

37+
@Inject
38+
private lateinit var springControllerAnnotationResolver: SpringControllerAnnotationResolver
39+
3740
override fun processClass(cls: PsiClass, classExportContext: ClassExportContext) {
3841

3942
val ctrlRequestMappingAnn = findRequestMappingInAnn(cls)
@@ -50,9 +53,8 @@ open class SpringRequestClassExporter : RequestClassExporter() {
5053
}
5154

5255
override fun hasApi(psiClass: PsiClass): Boolean {
53-
return SpringClassName.SPRING_CONTROLLER_ANNOTATION.any {
54-
annotationHelper.hasAnn(psiClass, it)
55-
} || (ruleComputer.computer(ClassExportRuleKeys.IS_SPRING_CTRL, psiClass) ?: false)
56+
return springControllerAnnotationResolver.hasControllerAnnotation(psiClass) ||
57+
(ruleComputer.computer(ClassExportRuleKeys.IS_SPRING_CTRL, psiClass) ?: false)
5658
}
5759

5860
override fun isApi(psiMethod: PsiMethod): Boolean {

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringRequestMappingResolver.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ package com.itangcent.idea.plugin.api.export.spring
33
import com.google.inject.ImplementedBy
44
import com.intellij.psi.PsiElement
55

6+
/*
7+
* This interface defines a contract for resolving Spring request mapping annotations.
8+
* It is responsible for extracting mapping information from Spring MVC request mapping
9+
* annotations on classes and methods. This includes both standard Spring annotations
10+
* and custom annotations that may be used for request mapping.
11+
*/
612
@ImplementedBy(DefaultSpringRequestMappingResolver::class)
713
interface SpringRequestMappingResolver {
814

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.itangcent.idea.plugin.api.export.spring
2+
3+
import com.google.inject.Inject
4+
import com.intellij.psi.PsiClass
5+
import com.itangcent.intellij.jvm.AnnotationHelper
6+
7+
/*
8+
* This class provides a standard implementation for resolving whether a given PsiClass
9+
* has a Spring controller annotation.
10+
*/
11+
class StandardSpringControllerAnnotationResolver : SpringControllerAnnotationResolver {
12+
13+
@Inject
14+
private lateinit var annotationHelper: AnnotationHelper
15+
16+
override fun hasControllerAnnotation(psiClass: PsiClass): Boolean {
17+
// Check for direct Spring controller annotations
18+
return SpringClassName.SPRING_CONTROLLER_ANNOTATION.any { annotationHelper.hasAnn(psiClass, it) }
19+
}
20+
}

0 commit comments

Comments
 (0)