Annotations are metadata markers that can be attached to classes, members, and methods. They do not affect execution by default but can be read at runtime using the reflect module. This makes them useful for building frameworks, documentation generators, validation engines, and other tools that need to inspect code structure.
An annotation starts with @ followed by the annotation name. Annotations may optionally include named key-value arguments inside parentheses. All argument values are strings.
Without arguments:
@Before
public setUp() { }
With one argument:
@Test(name = "My test name")
public myTest() { }
With multiple arguments:
@Route(method = "GET", path = "/api/users")
public getUsers() { }
Multiple annotations can be stacked on the same declaration.
Annotations can be placed on three kinds of declarations.
@Test(name = "Calculator Tests")
class calculatorTests {
// ...
}
class product {
@Required
public name = "";
@Min(value = "0")
@Max(value = "9999")
public price = 0.0;
@Mock
public service;
}
class orderController {
@Route(method = "GET", path = "/orders")
public listOrders() { }
@Route(method = "POST", path = "/orders")
@Auth(role = "admin")
public createOrder() { }
}
To read annotations at runtime you need the reflect module.
include reflect;
The main entry point is reflect.getClassDef(ClassName), which returns an RClass object for the named class. From there you can call getMembers() and getMethods() to get full definitions including any annotations.
Note: Some reflection functions require security manager permissions. The
evalStr,evalFile, andincludeModulefunctions are gated by thereflect.eval.string,reflect.eval.file, andreflect.include.moduleproperties respectively, and are disabled by default.getClassDefand related introspection functions may also be restricted depending on how the security manager is configured in your environment. Check with your Aussom host application if reflection calls are not working as expected.
Each annotation is represented as a map with two keys:
| Key | Type | Description |
|---|---|---|
annotationName |
string | The name of the annotation without the @ prefix. |
annotationArgs |
map | A map of key-value pairs for the annotation's arguments. Keys and values are both strings. If the annotation has no arguments this map is empty. |
Example for @Route(method = "GET", path = "/orders"):
{
annotationName: "Route",
annotationArgs: {
method: "GET",
path: "/orders"
}
}
Example for @Before (no arguments):
{
annotationName: "Before",
annotationArgs: {}
}
The annotations field on a member or method definition is a list of these annotation maps, so a declaration with multiple annotations will have multiple entries in the list.
RClass.getMembers() returns a map where each key is a member name and the value is a definition map with the following fields:
| Key | Type | Description |
|---|---|---|
type |
string | The declared type of the member (e.g. STRING, INT, NULL, UNDEF). |
annotations |
list | A list of annotation maps for this member. |
Example:
include reflect;
class product {
@Required
public name = "";
@Min(value = "0")
@Max(value = "9999")
public price = 0.0;
public description = "";
}
class annotationExample {
public main(args) {
cls = reflect.getClassDef("product");
members = cls.getMembers();
for (memberName : members) {
memberDef = members[memberName];
annotations = memberDef["annotations"];
if (#annotations > 0) {
c.info("Member: " + memberName);
for (ann : annotations) {
c.info(" @" + ann["annotationName"]);
annArgs = ann["annotationArgs"];
for (argKey : annArgs) {
c.info(" " + argKey + " = " + annArgs[argKey]);
}
}
}
}
}
}
Running this would output something like:
Member: name
@Required
Member: price
@Min
value = 0
@Max
value = 9999
RClass.getMethods() returns a map where each key is a method name and the value is a definition map with the following fields:
| Key | Type | Description |
|---|---|---|
isExtern |
bool | Whether the method is an extern method. |
annotations |
list | A list of annotation maps for this method. |
arguments |
list | A list of argument definition maps (see below). |
Each argument definition map contains:
| Key | Type | Description |
|---|---|---|
name |
string | The argument name, or "..." for variadic arguments. |
requiredType |
string | The declared type constraint (e.g. "string", "int"), or empty string if untyped. |
hasDefaultValue |
bool | Whether a default value is defined. |
defaultValueType |
string | The type of the default value (present when hasDefaultValue is true). |
defaultValue |
string | The string representation of the default value (present when hasDefaultValue is true). |
Example:
include reflect;
class orderController {
@Route(method = "GET", path = "/orders")
public listOrders() { }
@Route(method = "POST", path = "/orders")
@Auth(role = "admin")
public createOrder(string payload) { }
}
class annotationExample {
public main(args) {
cls = reflect.getClassDef("orderController");
methods = cls.getMethods();
for (methodName : methods) {
methodDef = methods[methodName];
annotations = methodDef["annotations"];
if (#annotations > 0) {
c.info("Method: " + methodName);
for (ann : annotations) {
c.info(" @" + ann["annotationName"]);
annArgs = ann["annotationArgs"];
for (argKey : annArgs) {
c.info(" " + argKey + " = " + annArgs[argKey]);
}
}
}
}
}
}
Output:
Method: listOrders
@Route
method = GET
path = /orders
Method: createOrder
@Route
method = POST
path = /orders
@Auth
role = admin
A common pattern is to collect all methods or members that carry a specific annotation.
include reflect;
class myRouter {
@Route(method = "GET", path = "/users")
public getUsers() { }
@Route(method = "GET", path = "/users/profile")
public getProfile() { }
public internalHelper() { }
}
class annotationExample {
public main(args) {
cls = reflect.getClassDef("myRouter");
methods = cls.getMethods();
routes = [];
for (methodName : methods) {
methodDef = methods[methodName];
for (ann : methodDef["annotations"]) {
if (ann["annotationName"] == "Route") {
routes @= {
method: methodName,
httpMethod: ann["annotationArgs"]["method"],
path: ann["annotationArgs"]["path"]
};
}
}
}
c.info("Discovered routes:");
for (route : routes) {
c.info(" " + route["httpMethod"] + " " + route["path"] + " -> " + route["method"] + "()");
}
}
}
Output:
Discovered routes:
GET /users -> getUsers()
GET /users/profile -> getProfile()
The following brings all the concepts together: annotated classes, members, and methods, reading them back via reflection, and checking for specific annotations.
include reflect;
// --- Annotated code ---
@Entity(table = "products")
class product {
@Id
public id = 0;
@Column(name = "product_name")
@Required
public name = "";
@Column(name = "unit_price")
@Min(value = "0")
public price = 0.0;
@Transient
public tempNote = "";
}
// --- Reflection consumer ---
class schemaInspector {
public main(args) {
cls = reflect.getClassDef("product");
// Read class-level annotations
c.info("Class: " + cls.getName());
// Inspect all members
members = cls.getMembers();
c.info("Members:");
for (memberName : members) {
memberDef = members[memberName];
annotations = memberDef["annotations"];
annNames = [];
for (ann : annotations) {
annNames @= "@" + ann["annotationName"];
}
line = " " + memberName + " [" + memberDef["type"] + "]";
if (#annNames > 0) {
line = line + " " + annNames.join(", ");
}
c.info(line);
}
// Find columns (members with @Column)
c.info("Columns:");
for (memberName : members) {
for (ann : members[memberName]["annotations"]) {
if (ann["annotationName"] == "Column") {
colName = ann["annotationArgs"]["name"];
c.info(" " + memberName + " -> column '" + colName + "'");
}
}
}
}
}
Output:
Class: product
Members:
id [INT] @Id
name [STRING] @Column, @Required
price [DOUBLE] @Column, @Min
tempNote [STRING] @Transient
Columns:
name -> column 'product_name'
price -> column 'unit_price'