Skip to content

Commit c426484

Browse files
authored
Merge pull request #2734 from hongwei1/feature/ttksandbox
bugfix/use Reflections instead of ClassUtil for better Fat JAR class scanning
2 parents adff5c1 + b77b4be commit c426484

5 files changed

Lines changed: 77 additions & 173 deletions

File tree

obp-api/pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,11 @@
271271
<version>4.0.3</version>
272272
</dependency>
273273
<!-- ********** flexmark END ********** -->
274-
<!--scala utils, for type scan-->
274+
<!--class scanning, replaces classutil for better Fat JAR support-->
275275
<dependency>
276-
<groupId>org.clapper</groupId>
277-
<artifactId>classutil_${scala.version}</artifactId>
278-
<version>1.5.1</version>
276+
<groupId>org.reflections</groupId>
277+
<artifactId>reflections</artifactId>
278+
<version>0.10.2</version>
279279
</dependency>
280280
<dependency>
281281
<groupId>com.github.grumlimited</groupId>

obp-api/src/main/scala/bootstrap/liftweb/Boot.scala

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,9 @@ import code.api.ResourceDocs1_4_0.ResourceDocs300.{ResourceDocs310, ResourceDocs
4141
import code.api.ResourceDocs1_4_0._
4242
import code.api._
4343
import code.api.attributedefinition.AttributeDefinition
44-
import code.api.berlin.group.v1_3.{OBP_BERLIN_GROUP_1_3, OBP_BERLIN_GROUP_1_3_Alias}
4544
import code.api.berlin.group.ConstantsBG
46-
import code.api.STET.v1_4.OBP_STET_1_4
47-
import code.api.Polish.v2_1_1_1.OBP_PAPI_2_1_1_1
48-
import code.api.MxOF.{OBP_MXOF_1_0_0, CNBV9_1_0_0}
49-
import code.api.BahrainOBF.v1_0_0.{ApiCollector => BahrainApiCollector}
50-
import code.api.AUOpenBanking.v1_0_0.{ApiCollector => AUApiCollector}
51-
import code.api.UKOpenBanking.v2_0_0.OBP_UKOpenBanking_200
52-
import code.api.UKOpenBanking.v3_1_0.OBP_UKOpenBanking_310
5345
import code.api.cache.Redis
54-
import code.api.util.APIUtil.{enableVersionIfAllowed, versionIsAllowed,errorJsonResponse, getPropsValue}
46+
import code.api.util.APIUtil.{enableVersionIfAllowed, errorJsonResponse, getPropsValue}
5547
import code.api.util.ApiRole._
5648
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
5749
import code.api.util._
@@ -475,95 +467,7 @@ class Boot extends MdcLoggable {
475467
ApiVersion.setUrlPrefix(ApiPathZero)
476468

477469
// Add the various API versions
478-
val scannedApisCount = ScannedApis.versionMapScannedApis.size
479-
logger.info(s"ClassScanUtils found $scannedApisCount ScannedApis implementations")
480-
481470
ScannedApis.versionMapScannedApis.keys.foreach(enableVersionIfAllowed) // process all scanned apis versions
482-
483-
484-
// Manual registration for ScannedApis if not already registered by ClassScanUtils
485-
// This ensures all APIs work in Fat JAR environment where class scanning fails
486-
487-
if (!ScannedApis.versionMapScannedApis.contains(ConstantsBG.berlinGroupVersion1)) {
488-
logger.warn("BGv1.3 was NOT found by ClassScanUtils, registering manually")
489-
if (versionIsAllowed(ConstantsBG.berlinGroupVersion1)) {
490-
LiftRules.statelessDispatch.append(OBP_BERLIN_GROUP_1_3)
491-
logger.info(s"${ConstantsBG.berlinGroupVersion1.fullyQualifiedVersion} was ENABLED (manual registration)")
492-
}
493-
}
494-
495-
if (!ScannedApis.versionMapScannedApis.contains(OBP_BERLIN_GROUP_1_3_Alias.apiVersion)) {
496-
logger.warn("BGv1.3 Alias was NOT found by ClassScanUtils, registering manually")
497-
if (versionIsAllowed(OBP_BERLIN_GROUP_1_3_Alias.apiVersion)) {
498-
LiftRules.statelessDispatch.append(OBP_BERLIN_GROUP_1_3_Alias)
499-
logger.info(s"${OBP_BERLIN_GROUP_1_3_Alias.apiVersion.fullyQualifiedVersion} was ENABLED (manual registration)")
500-
}
501-
}
502-
503-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.stetV14)) {
504-
logger.warn("STET v1.4 was NOT found by ClassScanUtils, registering manually")
505-
if (versionIsAllowed(ApiVersion.stetV14)) {
506-
LiftRules.statelessDispatch.append(OBP_STET_1_4)
507-
logger.info(s"${ApiVersion.stetV14.fullyQualifiedVersion} was ENABLED (manual registration)")
508-
}
509-
}
510-
511-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.polishApiV2111)) {
512-
logger.warn("Polish API v2.1.1.1 was NOT found by ClassScanUtils, registering manually")
513-
if (versionIsAllowed(ApiVersion.polishApiV2111)) {
514-
LiftRules.statelessDispatch.append(OBP_PAPI_2_1_1_1)
515-
logger.info(s"${ApiVersion.polishApiV2111.fullyQualifiedVersion} was ENABLED (manual registration)")
516-
}
517-
}
518-
519-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.mxofV100)) {
520-
logger.warn("Mexico Open Finance v1.0.0 was NOT found by ClassScanUtils, registering manually")
521-
if (versionIsAllowed(ApiVersion.mxofV100)) {
522-
LiftRules.statelessDispatch.append(OBP_MXOF_1_0_0)
523-
logger.info(s"${ApiVersion.mxofV100.fullyQualifiedVersion} was ENABLED (manual registration)")
524-
}
525-
}
526-
527-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.cnbv9)) {
528-
logger.warn("Mexico CNBV9 v1.0.0 was NOT found by ClassScanUtils, registering manually")
529-
if (versionIsAllowed(ApiVersion.cnbv9)) {
530-
LiftRules.statelessDispatch.append(CNBV9_1_0_0)
531-
logger.info(s"${ApiVersion.cnbv9.fullyQualifiedVersion} was ENABLED (manual registration)")
532-
}
533-
}
534-
535-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.bahrainObfV100)) {
536-
logger.warn("Bahrain OBF v1.0.0 was NOT found by ClassScanUtils, registering manually")
537-
if (versionIsAllowed(ApiVersion.bahrainObfV100)) {
538-
LiftRules.statelessDispatch.append(BahrainApiCollector)
539-
logger.info(s"${ApiVersion.bahrainObfV100.fullyQualifiedVersion} was ENABLED (manual registration)")
540-
}
541-
}
542-
543-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.cdsAuV100)) {
544-
logger.warn("Australia CDS v1.0.0 was NOT found by ClassScanUtils, registering manually")
545-
if (versionIsAllowed(ApiVersion.cdsAuV100)) {
546-
LiftRules.statelessDispatch.append(AUApiCollector)
547-
logger.info(s"${ApiVersion.cdsAuV100.fullyQualifiedVersion} was ENABLED (manual registration)")
548-
}
549-
}
550-
551-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.ukOpenBankingV20)) {
552-
logger.warn("UK Open Banking v2.0.0 was NOT found by ClassScanUtils, registering manually")
553-
if (versionIsAllowed(ApiVersion.ukOpenBankingV20)) {
554-
LiftRules.statelessDispatch.append(OBP_UKOpenBanking_200)
555-
logger.info(s"${ApiVersion.ukOpenBankingV20.fullyQualifiedVersion} was ENABLED (manual registration)")
556-
}
557-
}
558-
559-
if (!ScannedApis.versionMapScannedApis.contains(ApiVersion.ukOpenBankingV31)) {
560-
logger.warn("UK Open Banking v3.1.0 was NOT found by ClassScanUtils, registering manually")
561-
if (versionIsAllowed(ApiVersion.ukOpenBankingV31)) {
562-
LiftRules.statelessDispatch.append(OBP_UKOpenBanking_310)
563-
logger.info(s"${ApiVersion.ukOpenBankingV31.fullyQualifiedVersion} was ENABLED (manual registration)")
564-
}
565-
}
566-
567471
enableVersionIfAllowed(ApiVersion.v1_2_1)
568472
enableVersionIfAllowed(ApiVersion.v1_3_0)
569473
enableVersionIfAllowed(ApiVersion.v1_4_0)

obp-api/src/main/scala/code/api/util/ApiVersionUtils.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import code.api.berlin.group.ConstantsBG
77
object ApiVersionUtils {
88

99
val scannedApis = ScannedApis.versionMapScannedApis.keysIterator.toList
10-
val versions =
10+
val versions = (
1111
v1_2_1 ::
1212
v1_3_0 ::
1313
v1_4_0 ::
@@ -25,6 +25,7 @@ object ApiVersionUtils {
2525
`dynamic-entity` ::
2626
ConstantsBG.berlinGroupVersion2 ::
2727
scannedApis
28+
).distinct
2829

2930
def valueOf(value: String): ScannedApiVersion = {
3031

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,113 @@
11
package code.util
22

3-
import java.io.File
4-
5-
import com.openbankproject.commons.model.Bank
63
import code.util.Helper.MdcLoggable
74
import org.apache.commons.lang3.StringUtils
8-
import org.clapper.classutil.{ClassFinder, ClassInfo}
5+
import org.reflections.Reflections
6+
import org.reflections.scanners.Scanners
7+
import org.reflections.util.{ClasspathHelper, ConfigurationBuilder}
98
import com.openbankproject.commons.util.ReflectUtils
109

10+
import scala.jdk.CollectionConverters._
1111
import scala.reflect.runtime.universe.TypeTag
1212

1313
/**
14-
* this is some util method to scan any class according some rules
14+
* Utility methods to scan classes using Reflections library.
15+
* Replaces classutil (org.clapper) which does not support Fat JAR environments.
1516
* @author shuang
1617
*/
1718
object ClassScanUtils extends MdcLoggable {
1819

19-
lazy val finder = ClassFinder(getClassPath(this.getClass, classOf[Bank], classOf[String]))
20+
// Scan the "code" package only to avoid scanning all dependencies
21+
lazy val reflections: Reflections = {
22+
val config = new ConfigurationBuilder()
23+
.setUrls(ClasspathHelper.forPackage("code"))
24+
.setScanners(Scanners.SubTypes.filterResultsBy(_ => true))
25+
new Reflections(config)
26+
}
2027

2128
/**
2229
* get companion object or singleton object by class name
2330
* @param name object class name
2431
* @tparam U expect type
2532
* @return companion object or singleton object
2633
*/
27-
def companion[U:TypeTag](name : String) : U = {
28-
val className = if(name.endsWith("$")) name else name + "$"
34+
def companion[U: TypeTag](name: String): U = {
35+
val className = if (name.endsWith("$")) name else name + "$"
2936
Class.forName(className).getDeclaredField("MODULE$").get(null).asInstanceOf[U]
3037
}
3138

3239
/**
3340
* scan classpath to get all companion objects or singleton objects those implements given trait
3441
* @tparam T the trait type parameter
35-
* @return all companion objects or singleton object those implements given clazz
42+
* @return all companion objects or singleton objects those implement the given trait
3643
*/
37-
def getSubTypeObjects[T:TypeTag]: List[T] = {
44+
def getSubTypeObjects[T: TypeTag]: List[T] = {
3845
val clazz = ReflectUtils.typeTagToClass[T]
39-
val classes = try {
40-
val allClasses = finder.getClasses().toList
41-
logger.info(s"ClassScanUtils successfully scanned ${allClasses.size} classes from classpath")
42-
allClasses
46+
try {
47+
val subTypes = reflections.getSubTypesOf(clazz).asScala.toList
48+
logger.info(s"ClassScanUtils (Reflections) found ${subTypes.size} subtypes of ${clazz.getName}")
49+
// companion objects have a class name ending with "$"
50+
val objects = subTypes
51+
.filter(c => c.getName.endsWith("$"))
52+
.flatMap { c =>
53+
try { Some(companion[T](c.getName)) }
54+
catch { case e: Exception =>
55+
logger.warn(s"Failed to load companion object ${c.getName}: ${e.getMessage}")
56+
None
57+
}
58+
}
59+
logger.info(s"Found ${objects.size} companion objects implementing ${clazz.getName}")
60+
objects
4361
} catch {
44-
case e: UnsupportedOperationException =>
45-
// ASM version is too old for some class files (e.g. requires ASM7). In that case,
46-
// skip scanned APIs instead of failing the whole application.
47-
logger.warn(s"Class scanning failed with UnsupportedOperationException: ${e.getMessage}")
48-
logger.warn("This is expected when running from a Fat JAR. Scanned APIs will not be auto-registered.")
49-
Seq.empty
5062
case e: Exception =>
51-
logger.warn(s"Class scanning failed with ${e.getClass.getSimpleName}: ${e.getMessage}")
52-
Seq.empty
63+
logger.warn(s"ClassScanUtils (Reflections) failed for ${clazz.getName}: ${e.getMessage}")
64+
Nil
5365
}
54-
val filtered = classes.filter(_.implements(clazz.getName))
55-
logger.info(s"Found ${filtered.size} classes implementing ${clazz.getName}")
56-
filtered.map(_.name).map(companion[T](_)).toList
5766
}
5867

5968
/**
60-
* find all fit classes, do filter with predict function
61-
* @param predict check whether include this type in the result
62-
* @return all fit type names
69+
* find all fit classes, filtered by a predicate on the Class object.
70+
* @param predict check whether to include this class in the result
71+
* @return all matching class names (without trailing "$")
6372
*/
64-
def findTypes(predict: ClassInfo => Boolean): List[String] = {
65-
val classes = try {
66-
finder.getClasses().toList
73+
def findTypes(predict: Class[_] => Boolean): List[String] = {
74+
try {
75+
// getSubTypesOf(Object) returns all known classes in the scanned packages
76+
reflections.getSubTypesOf(classOf[Object]).asScala.toList
77+
.filter { c =>
78+
try { predict(c) }
79+
catch { case _: Exception => false }
80+
}
81+
.map { c =>
82+
val name = c.getName
83+
if (name.endsWith("$")) name.substring(0, name.length - 1) else name
84+
}
6785
} catch {
68-
case _: UnsupportedOperationException =>
69-
Seq.empty
86+
case e: Exception =>
87+
logger.warn(s"ClassScanUtils.findTypes failed: ${e.getMessage}")
88+
Nil
7089
}
71-
classes
72-
.filter(predict)
73-
.map(it => {
74-
val name = it.name
75-
if(name.endsWith("$")) name.substring(0, name.length - 1)
76-
else name
77-
}) //some companion type name ends with $, it added by scalac, should remove from class name
78-
.toList
7990
}
8091

8192
/**
82-
* get given class exists jar Files
83-
* @param classes to find class paths contains these class files
84-
* @return this class exists jar File
85-
*/
86-
private[this] def getClassPath(classes: Class[_]*): Seq[File] = classes.map { clazz =>
87-
val classFile = "/" + clazz.getName.replace('.', '/') + ".class"
88-
val uri = clazz.getResource(classFile).toURI.toString
89-
val path = uri.replaceFirst("^(jar:|file:){0,2}(.*?)\\!?\\Q" + classFile + "\\E$", "$2")
90-
new File(path)
91-
}
92-
93-
/**
94-
* get all subtype of net.liftweb.mapper.LongKeyedMapper, so we can register scanned db models dynamic
93+
* get all subtype of net.liftweb.mapper.LongKeyedMapper, so we can register scanned db models dynamically
9594
* @param packageName scanned root package name
96-
* @return all scanned ClassInfo
95+
* @return all matching class names
9796
*/
98-
def getMappers(packageName:String = ""): Seq[ClassInfo] = {
99-
val mapperInterface = "net.liftweb.mapper.LongKeyedMapper"
100-
val classes = try {
101-
finder.getClasses().toList
97+
def getMappers(packageName: String = ""): Seq[String] = {
98+
try {
99+
val mapperInterface = Class.forName("net.liftweb.mapper.LongKeyedMapper")
100+
val all = reflections.getSubTypesOf(mapperInterface).asScala.toSeq
101+
.map(_.getName)
102+
if (StringUtils.isNotBlank(packageName))
103+
all.filter(_.startsWith(packageName))
104+
else
105+
all
102106
} catch {
103-
case _: UnsupportedOperationException =>
104-
Seq.empty
105-
}
106-
val infos = classes.filter(it => it.interfaces.contains(mapperInterface))
107-
if(StringUtils.isNoneBlank()) {
108-
infos.filter(classInfo => classInfo.name.startsWith(packageName))
109-
} else {
110-
infos
107+
case e: Exception =>
108+
logger.warn(s"ClassScanUtils.getMappers failed: ${e.getMessage}")
109+
Nil
111110
}
112111
}
113112

114-
}
113+
}

obp-api/src/test/scala/code/util/MappedClassNameTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ class MappedClassNameTest extends FeatureSpec {
120120
"code.CustomerDependants.MappedCustomerDependant",
121121
)
122122

123-
val newMappedTypes = ClassScanUtils.findTypes{ info =>
124-
val typeName = info.name
123+
val newMappedTypes = ClassScanUtils.findTypes{ clazz =>
124+
val typeName = clazz.getName
125125
!typeName.endsWith("$") &&
126126
!oldMappedTypeNames.contains(typeName) &&
127-
mapperClazz.isAssignableFrom(Class.forName(typeName, false, mapperClazz.getClassLoader))
127+
mapperClazz.isAssignableFrom(clazz)
128128
}.toSet
129129
feature("Validate New Entity name and column name") {
130130

0 commit comments

Comments
 (0)