jackadull / objectscan   0.4.0

MIT License GitHub

A small utility library for quickly finding all singleton instances of a given type, including subtypes.

Scala versions: 2.13

Objectscan

Travis CI Maven Central Codefactor Snyk

A small utility library for quickly finding all singleton instances of a given type, including subtypes.

Internally, it uses ClassGraph for performing top-speed classpath scans. However, this is an implementation detail which may change in future versions, or it may not change. Users of Objectscan should only use the simple interface which it exposes to the outside world, and not rely on any implementation specifics.

Dependency management and compatibility

Objectscan is compatible with Scala 2.13 . Best effort is made to always keep it up-to-date with the latest Scala version.

Cross-versioning will not be supported. When a new Scala version is released, your code should be updated to that version as soon as possible anyways.

SBT

libraryDependencies += "net.jackadull" %% "objectscan" % "0.4.0"

Maven

<dependency>
  <groupId>net.jackadull</groupId>
  <artifactId>objectscan_2.13</artifactId>
  <version>0.4.0</version>
</dependency>

Use cases

Use of this utility library is somewhat focused on a certain use case: It will find all singleton instances of a certain type, optionally with certain restrictions. This is achieved by scanning the classpath.

This can be very useful for cases where it is necessary to enumerate all instances of a certain type, for example for enumeration-like types. When used this way, this can spare manually keeping a list of all instances in the source code, and automate the process of finding all instances at runtime, with only little overhead. This reduces the need of code maintenance, and the chance of making mistakes.

Another possible use case is for projects that are just static text generators. In such projects, a certain trait can mark a chapter, or a web page. The scanner can then be used for finding all chapters or pages dynamically, potentially rendering them. In this way, new elements can be created simply by creating a singleton, without the need to register them anywhere.

How Singletons are provided and found

Objectscan finds singleton instances. By "singleton", this document mostly means a typical Scala singleton, i.e. a Scala object .

However, not all singletons can be conveniently defined as object . Sometimes, it can be convenient to specify an algorithm that generates a number of instances.

For such cases, the ObjectScanSource trait may be implemented:

trait TrafficLightColor {def name:String}
object TrafficLightColor extends ObjectScanSource {
  def scannableObjects = Seq("red", "green", "yellow").map(color => new TrafficLightColor {def name = color})
}

This will simply inject the returned scannableObjects into the pool of scannable singletons, and behave as if they where defined as Scala object , each respectively. When a scan is restricted by package prefix, the package prefix of the enclosing ObjectScanSource counts for that restriction.

Note that it is possible to use Objectscan inside a scannableObjects implementation. However, such cases must be handled with great care: Objectscan does not handle recursions gracefully. The developer must make sure that no recursions can occur on a path of ObjectScanSources that are linked to each other.

Also take into account that scannableObjects must always return the same result, no matter when or how many times it gets called. The reason is given below, under the heading "Stability assumption".

Not to be used for DI

However, Objectscan is not useful for dependency injection. It offers no way for conflict resolution or scope management.

For example: Suppose you define a trait DatabaseAccess , which is an abstract model for accessing the database. Instead of hard-wiring to a static instance, you use Objectscan for finding the single instance of DatabaseAccess , and then later on, to use that instance. One module defines a MySQLDatabaseAccess singleton, which is found correctly. So far, everything looks fine.

But then, another module defines MongoDBDatabaseAccess , because it stores reporting data in a Mongo DB. All of a sudden, those two modules cannot be used together, because they introduce two DatabaseAccess singletons to the classpath.

Even though this case could in theory be resolved, by adding your own discriminator methods for resolving scope, there are more arguments why Objectscan is not suited for DI:

  • It is not very good at error handling. If your database connection cannot be established, the error will be hard to trace.

  • It does not handle the case of circular dependencies.

  • It does not offer any configuration options, for example, for injecting your database password from a secure source.

Usage

Objectscan relies on the assumption that all scans yield the same result during the JVM lifetime, no matter at what time they are called. This also implies that all scan results can be cached, as future calls with the same parameters would return the same results anyway.

The stability assumption can in theory be violated (by implementing ObjectScanSource in an unstable way), which is strongly advised against. This would lead to inconsistent behavior of the application.

Basic usage

Creating an instance of All

The main entry point for all code using Objectscan is net.jackadull.objectscan.All . Creating an instance of All does not yet initiate any scanning operation. It is just a preparation for upcoming scans.

Probably, you may want to reuse your All instance, because it may cache its results.

At first, an instance of All must be created with some limiting factor, or otherwise classpath scans of bigger projects can become very inefficient. Usually, you can either limit the package prefix of the scanned objects, or determine that they must be nested inside another singleton.

import net.jackadull.objectscan.All
val all = All.withinPackagePrefixes(Seq("net.jackadull.example", "com.mycompany"))

The above example will scan all class files whose package begins with either net.jackadull.exampe or com.mycompany .

object MyEnclosingSingleton 
val all = All.nestedInside(MyEnclosingSingleton)

This will only scan all class files that are nested inside MyEnclosingSingleton .

Note: If you really want to scan the whole classpath, use All.withinPackagePrefixes(Seq()) . Use at your own risk though, as these scans grow proportionally with classpath size.

Running scans

Here are some examples for classpath scans:

val all = ??? // see above
all.of[SomeType].toSeq               // returns Seq[SomeType]
all.of[SomeType].where(_.isActive)   // returns a filtered Seq[SomeType]
all.of[SomeType].isEmpty             // true if there are no instances of SomeType
all.of[SomeType].nonEmpty            // true if there is at least one singleton instance of SomeType

The examples above give a good overview over the possible operations on All instances.