Julia Documentation
See:
Description
Table of Contents
1 Introduction
2 Design goals
3 Code structure
4 Main data structures
5 Mixin classes
6 Interceptor classes
7 Class generation framework
8 Support for constrained environments
A Mixins reference
B Optimizations
1 Introduction
Julia is France Telecom's implementation of the Fractal Specification.
This document describes the design choices made to implement Julia, the
organization of Julia's source code, and the main algorithms and data
structures used in Julia. This document is intended for framework developpers
that want to extend Julia, but it can also be useful for component developpers
that want to understand how Julia works. The reader is supposed to be familiar
with the Fractal Specification.
2 Design Goals
The main design goal is to implement a framework to program component
controllers: we want to implement an extensible set of control objects, from
which the user can freely choose and assemble the controller objects he or she
wants, in order to build the controller part of a Fractal component.
The second goal is to provide a complete continuum from static
configuration to dynamic reconfiguration: we want to provide a sufficiently
rich and flexible set of control objects, so that the user can make the
speed/memory tradeoffs he or she wants. In particular, this set of control
objects must allow the user to instantiate non reconfigurable but very efficent
components or, on the contrary, completely reconfigurable but less efficient
components. It must also allow the user to mix these components, in order to use
different optimization levels for the different parts of an application. The
last requirement is to be able to dynamically deoptimize an optimized component,
in order to make it reconfigurable, and to be able to dynamically reoptimize it
after it has been reconfigured.
The third goal is to implement these control objects so as to minimize the
time overhead due to these objects on user applications and to optimize the
performances of the methods of the Fractal API. The memory overhead of the
controller objects was not a priority (indeed, in many cases, the number of
components is less than one hundred or a thousand of components).
The last goal is to implement a framework that can be used on any JVM and/or
JDK, including very constrained ones such as the KVM and the J2ME profile (where
there is no ClassLoader class, no reflection API, no collection
API...).
In addition to the previous design goals, we also make two hypotheses in
order to simplify the implementation:
- concurrency: we suppose there is only one (re)configuration thread at a
given time.
- protection: Julia's internal data structures are not protected from
malicious user components.
3 Code Structure
The source code of Julia is organized in three parts:
- julia-runtime.jar: this part contains the minimum classes that are
needed at runtime. These classes are exception classes, some Java interfaces,
as well as some utility classes providing useful static methods.
- julia-mixins.jar: this part contains implementations of the Fractal
API interfaces, as a set of mixin classes (see section 5)
that can be composed in many ways.
- julia-asm.jar: this part contains the classes that are used to
generate application specific classes that are needed by Julia (these
application specific classes can be compared to stub and skeleton classes in
ORBs).
By default, when a Fractal application is launched with Julia, the generator
classes dynamically generate the needed application specific classes, as well as
the needed controller classes (by mixing some or all of the mixins classes).
These classes are then used to construct the application's components. If these
generated classes are stored on disk and included in the classpath, then the
application can be constructed directly, without needing the generator and
mixins classes. In summary, Julia can be used in two different ways:
- with dynamic class generation: in this case the three julia-*.jar
files are needed at runtime, as well as the asm.jar file (and, of
course, the fractal.jar file - this gives a total of approximatively
190 KB jars). This case is not always possible: indeed, the code generator
classes heavily rely on Java's reflection API, which is not available on all
JVMs (they also need a ClassLoader class which is not always
available).
- with static class generation: in this case, in addition to the generated
classes, only the julia-runtime.jar and the fractal.jar
files are needed at runtime (this gives a total of approximatively 45 KB).
The Julia source is organized in several packages that have the same structure
as the Fractal API packages:
- the org.objectweb.fractal.julia package provides mixins classes
to implement the Component interface. It also provides an abstract
implementation of the Interface interface, which is used as a super
class to generate the application specific Interface classes.
- the org.objectweb.fractal.julia.control.* packages provide mixins
classes to implement the BindingController, ContentController,
LifeCycleController and NameController interfaces.
- the org.objectweb.fractal.julia.factory package provides mixins
classes to implement the Factory and GenericFactory
interfaces.
- the org.objectweb.fractal.julia.type package provides classes
that implement the interfaces of the corresponding Fractal API package.
- finally the org.objectweb.fractal.julia.loader package provides a
dynamic and a static loader to load the needed application specific
classes.
4 Main Data Structures
This section presents an overview of the data structures used in Julia to
represent Fractal components.
4.1 Overview
Figure 1: an abstract component and a possible implementation in Julia
A Fractal component is generally represented by many Java objects, which can
be separated into three groups (see Figure 1):
- the objects that implement the component interfaces, in red and green in
the above figure (one object per component interface; each object has an
"impl" link to an object that really implements the Java interface, and to
which all method calls are delegated; this reference is null for
client interfaces),
- the objects that implement the controller part of the component, in black
and brown in the figure (a controller object can implement zero of more
control interfaces),
- and the objects that implement the content part of the component (not
shown in the figure).
The fact that each component interface is represented by its own Java object
comes from the fact that component interfaces are typed (i.e., a component
interface object implements both Interface and the Java interface
corresponding to this interface). It is not possible to do better, unless
perhaps by using very complex bytecode manipulations that modify the signature
of all the methods of all classes (if i and j are interface
objects corresponding to two component interfaces named "iitf" and "jitf", then
i.getFcName() = "iitf" and j.getFcItfName() = "jitf".
i and j must therefore be distinct objects - if they were
physicaly equal, then i.getFcItfName() and j.getFcItfName()
would be logically equal too).
The objects that represent the controller part of a component can be
separated into two groups: the objects that implement the control interfaces (in
black in the above figure), and (optional) interceptor objects (in brown)
that intercept incoming and/or outgoing method calls on functional (or business,
or user) interfaces. These objects implement respectively the Controller
and the Interceptor interfaces. Each controller object can contain
references (in black) to other controller objects (since the control aspects are
generally not independent - or "orthogonal" - they must generally communicate
between each other, and must therefore have references to each other).
The objects that implement the content part of a component can be sub
components (for composite components), or user objects (for primitive
components). Note: Julia makes a distinction between the primitive
components that contain no user objects (such as primitive template components),
which are just called "primitive", and primitive components that contain a non
empty rooted graph of user objects, which are called "container"
(this name can lead to confusions with the fuzzy EJB and CCM container
definitions, but we do not have a better name to express the fact that these
components contain a non empty set of user objects).
4.2 Constraints
In order to reconfigure a component, it is sometimes necessary to replace an
object by another (for example to change an interceptor). The problem is that,
in order to do so, all the Java references to the object that must be replaced
must be changed also. And, in general, these references are not known. In order
to be able to reconfigure Fractal components, Julia makes the following
hypotheses:
- the objects encapsulated in container components, also called user
objects, only see component interface objects (*),
- component interface objects cannot be replaced (but their internal state
can be changed).
These hypotheses eliminate the unknowable references to controller and content
objects (i.e. the references from user objects to controller and content
objects, which can not be known from Julia), which can therefore be replaced if
necessary. Indeed, although there can be references between controller objects,
between component interface and controller objects, between interceptor and
"content" objects, and so on, all these references are managed by Julia, and are
therefore known.
These hypotheses also explain why the component interface objects are
separated from controller and interceptors objects: this is to be able to change
the controller and interceptor objects.
(*) the only exception is when user objects are bound directly to each other,
in a "static" configuration (see section 2.4 in the Julia tutorial). In this
case there may be unknowable references to content objects (but not to
controller or interceptor objects). In this case the application must not
disseminate these references, or must know and update them all when requested to
do so (via BindingController), or else there may be problems when a
user component is replaced by another.
4.3 Hidden Interfaces
Since the control aspects are generally not independent - or "orthogonal" - they
must generally communicate between each other, either inside a single component,
or between components. In these cases, in order to have a modular framework,
the interface and implementation separation principle is used (*): if a
controller object needs a service provided by another one, a Java interface is
defined for this service, which is implemented by the server object, and used in
the client object.
In order to be able to add protection mechanisms to Julia, or to be able to
provide distributed components on top of Julia (as Fractal RMI does), it was
also decided that the previous interfaces, only used inside Julia but not
visible from the outside, would be retrieved like any other component interface,
i.e., via the getFcInterface method of the Component
interface. The only difference is that these interfaces are hidden, i.e.,
they do not appear in the array returned by getFcInterfaces (hidden
interfaces are distinguished from normal interfaces by the fact that their name
starts with "/"). Therefore, one cannot get the reference to such an interface
without knowing its name. And, although not yet implemented, it is also possible
to perform access control checks in the getFcInterface method, to
ensure that only Julia's classes can access these hidden interfaces.
(*) a reference between an interceptor and a controller object may not follow
this principle, for performance reasons. For example, the lifecycle interceptor
object directly uses fields defined in the lifecycle controller object. In
practice this is not a problem, because the algorithms used in these two objects
are strongly coupled (i.e., the interceptor can not be changed without changing
the controller object, and vice versa).
4.4 Instantiation
Julia components can be created manually or automatically. The manual method
can be used to create any kind of components, while the automatic one is
restricted to components whose type follows the basic type system defined in the
Fractal specification, which provide a Component interface, and which
provide interface introspection functions. In both methods, a component must be
created as follows:
- creation of the component interface objects (if the component must provide
interface introspection), of the controller objects, of the interceptor
objects, and of the component's content (for container components).
- initialization of the "impl" references between the component interfaces
objects and the content, controller and interceptor objects.
- creation of an InitializationContext, and set up of this
context, with references to the previous objects.
- initialization of the controller and interceptor objects by calling their
initFcController method, with the previous
InitializationContext object as parameter (this step allows the
controller and interceptor objects to initialize themselves, i.e. to set up
the references between all these objects).
In the automatic method, i.e. when components are created through the
GenericFactory interface, the operations that must be done at the
previous steps are deduced from the component's type, and from its controller
and content descriptor. Once these descriptors have been analyzed and checked,
and once the previous operations have been determined, a sub class of the
InitilizationContext class that implements these operations is
generated (with the InitializationContextClassGenerator). Finally the
component is created by using this generated class (in other words, the
controller are content descriptors are compiled on the fly, once and for all,
instead of being interpreted and checked each time a component must be
created).
5 Mixin classes
5.1 Motivations
The main design goal of Julia is to implement a framework to program
component controllers (see section 2). In particular, since
everything in the Fractal specification is optional, Julia must provide
implementations of the Fractal API interfaces for any conformance level
(see the Fractal specification). For example, Julia must provide a
basic Component implementation, as well as an implementation for
components whose type follows the basic type system (in the first case
Component behaves like a read only hash map; in the second case,
because of collection interface types, the getFcInterface method can
lazily create new component interfaces). Likewhise, Julia must provide a basic
BindingController implementation, as well as an implementation for
cases where the basic type system is used, where a life cycle controller is
present, or where composite components are used (these implementations are
needed to check type, life cycle or content related constraints on bindings).
There must also be an implementation for cases where both the basic type system
and a life cycle controller are used, or where both the basic type system, life
cycle controllers and composite components are used. And these implementations
must be extensible, in order to take into account user defined controllers when
needed.
In order to provide all these implementations, a first solution would be to
use class inheritance. But this solution is not feasible, because it leads to a
combinatorial explosion, and to a lot of code duplication. Consider for example
the BindingController interface, and the "type system", "life cycle"
and "composite" concerns. These three concerns give 23=8 possible
combinations. Therefore, in order to implement these three concerns,
eight classes (and not just three) must be provided. Moreover these
eight classes can not be provided without duplicating code, if multiple
inheritance is not available.
Another solution to this problem would be to use an Aspect Oriented
Programming (AOP) tool or language, such as Aspect/J, since the goal of these
tools and languages is to solve the "crosscutting" problems. Aspect/J, for
example, could effectively be used to solve the above problem: three aspect
classes are sufficient to implement the three concerns, without any code
duplication. But using Aspect/J would introduce a new problem, due to the fact
that, in Aspect/J, aspects must be applied at compile time, and that this
process requires the source code of the "base" classes. It would then be
impossible to distribute Julia in compiled form, in a jar file, because then the
users would not be able to apply new aspects to the existing Julia classes (in
order to add new control aspects that crosscut existing ones).
What is needed to really solve our modularity and extensiblity problem is
therefore a kind of AOP tool or language that can be used at load time or at
runtime, without needing the source code of the base classes, such as JAC (Java
Aspect Components). For performance reasons the current Julia version does not
use JAC or other similar systems: it uses instead some kind of mixin
classes. A mixin class is a class whose super class is specified in an abstract
way, by specifying the minimum set of fields and methods it should have. A
mixin class can therefore be applied (i.e. override and add methods) to
any super class that defines at least these fields and methods. This property
solves the above combinatory problem. The Aspect/J problem is solved by the fact
that the mixin classes used in Julia can be applied at runtime (unlike in most
mixin based inheritance languages, where mixed classes are declared at compile
time).
5.2 Implementation
Instead of using a Java extension to program the mixin classes, which would
require an extended Java compiler or a pre processor, mixin classes in Julia are
programmed by using patterns. For example a mixin class, which, in JAM, would be written
as:
mixin A {
inherited public void m ();
public int count;
public void m () {
++count;
super.m();
}
}
is written in Julia in pure Java, as follows:
abstract class A {
abstract void _super_m ();
public int count;
public void m () {
++count;
_super_m();
}
}
In other words, the _super_ prefix is used to denote the
inherited members in JAM, i.e., the members that are required in a
base class, for the mixin class to be applicable to it. More precisely, the
_super_ prefix is used to denote methods that are overriden by the
mixin class. Members that are required but not overriden are denoted with
_this_ (a mixin class cannot contain a _super_m method if it
does not have a corresponding m method. Likewise, a mixin class cannot
have both a _this_m method and a corresponding m method):
abstract class M implements I, Countable {
int _this_f;
abstract void _super_m ();
abstract void _this_n ();
public int count;
public void m () {
++count;
_this_n();
_super_m();
}
public int getCount () {
return count;
}
}
Mixin classes can be mixed, resulting in normal classes. More precisely, the
result of mixing several mixin classes M1, ... Mn,
in this order, is a normal class that is equivalent to a class
Mn extending the Mn-1 class, itself extending the
Mn-2 class, ... itself extending the M1 class. Several
mixin classes can be mixed only if each method and field required by a mixin
class Mi is provided by a mixin class Mj, with j < i
(each required method and field may be provided by a different mixin). For
example, if N and O designate the following mixins:
abstract class N implements I {
abstract void _super_m ();
abstract void _super_n ();
public void m () {
System.out.println("m called");
_super_m();
}
public void n () {
System.out.println("n called");
_super_n();
}
}
abstract class O implements I {
public int f;
public void m () {
System.out.println("m");
}
public void n () {
System.out.println("n");
}
}
then the mixed class O N M is equivalent to the following class
(note that this class implements all the interfaces implemented by the
mixin classes):
public class ONM implements I, Countable {
// from O
public int f;
private void m$1 () {
System.out.println("m");
}
private void n$0 () {
System.out.println("n");
}
// from N
private void m$0 () {
System.out.println("m called");
m$1();
}
public void n () {
System.out.println("n called");
n$0();
}
// from M
public int count;
public void m () {
++count;
n();
m$0();
}
public int getCount () {
return count;
}
}
while the mixed class O M N is the following class:
public class OMN implements I, Countable {
// from O
public int f;
private void m$1 () {
System.out.println("m");
}
private void n$0 () {
System.out.println("n");
}
// from M
public int count;
private void m$0 () {
++count;
n();
m$1();
}
public int getCount () {
return count;
}
// from N
public void m () {
System.out.println("m called");
m$0();
}
public void n () {
System.out.println("n called");
n$0();
}
}
The mixed classes are generated dynamically by the MixinClassGenerator
class. Since this class can not mix constructors (this limitation could be
removed in future versions), a mixin class must not rely on constructors to
initialize its instances (in Julia, all mixin classes have a private empty
constructor without argument, to show that mixin classes should not be
instantiated directly). A mixed class can be declared as follows in the Julia
configuration file:
(onm
(org.objectweb.fractal.julia.asm.MixinClassGenerator
ONM
org.pkg.O
org.pkg.N
org.pkg.M
)
)
The first line after MixinClassGenerator is a symbolic name for the
mixed class. The following lines are the names of the classes to be mixed. In
order to ease debugging, the class generator keeps the line numbers of the mixin
classes in the mixed class. More precisely, a line number l of the mixin
class at index i (in the list of mixin classes, and starting from 1) is
transformed into 1000*i + l. For example, if a new
Exception().printStackTrace() were added in the N.m method,
the stack trace would contain the following line:
at C55d992cb_0.m$0(ONM:2010)
meaning that the exception was created in method m$0 of the
C55d992cb_0 class, whose source is the ONM mixed class (ONM comes from
the first line after MixinClassGenerator in the above descriptor), at
line 10 of mixin 2, i.e. at line 10 of the org.pkg.N mixin class.
6 Interceptor classes
This section gives some details about the generator used to generate interceptor
classes. This generator takes as parameters the name of a super class,
the name(s) of one or more application specific interface(s), and one or
more "aspect" code generator(s). It generates a sub class of the given
super class that implements all the given application specific interfaces
and that, for each application specific method, implements all the "aspects"
corresponding to the given "aspect" code generators.
Each "aspect" code generator can modify the code of each application specific
method arbitralily. For example, an "aspect" code generator A can modify
the method void m () { impl.m() } into:
void m () {
// pre code A
try {
impl.m();
} finally {
// post code A
}
}
while another "aspect" code generator B will modify this method into:
void m () {
// pre code B
impl.m();
// post code B
}
When an interceptor class is generated by using several "aspect" code
generators, the transformations performed by these code generators are
automatically composed together. For example, if A and B are used to generate
an interceptor class, in this order, the result for the previous m
method is the following:
void m () {
// pre code A
try {
// pre code B
impl.m();
// post code B
} finally {
// post code A
}
}
The order in which the aspects are "woven" is generally important, and
must be specified by the user of the code generator. For example, if A and B
are used in the reverse order, the result for the previous m method is
the following:
void m () {
// pre code B
// pre code A
try {
impl.m();
} finally {
// post code A
}
// post code B
}
Note that, thanks to this (elementary) automatic weaving, which is very similar
to what can be found in Aspect/J or in Composition Filters, several aspects can
be managed by a single interceptor object: there is no need, and there are
no chains of interceptor objects, each object corresponding to an aspect
(if one really wants interceptor chains, this can be done by implementing a new
interceptor class generator).
Like the controller objects, the aspects managed by the interceptor
objects of a given component can all be specified by the user when the
component is created. The user can therefore not only choose the control
interfaces he or she wants, but also the interceptor objects he or she
wants. Julia only provides two aspect code generators: one to manage the
lifecycle of components, the other to trace incoming and/or outgoing method
calls. Julia also provides two abstract code generators, named
SimpleCodeGenerator and MetaCodeGenerator, that can be easily
specialized (i.e. without needing to know ASM), in order to implement custom
code generators.
Note: each aspect code generator is given a Method object
corresponding to the method for which it must generate interception code. Thanks
to this argument, a code generator can generate code that adapts to the specific
signature of each method. It can also generate code to reify the method's
parameters if desired (although this is less efficient than the first method).
7 Class Generation Framework
Julia needs a lot of automatically generated classes, such as the component
factory classes (see section 4.4), the controller classes
generated from the mixin classes defined in Julia (see section
5), the classes that implement both Interface and an
application specific interface, the interceptor classes (see section 6), the merged controller classes (see appendix B), and so on.
Since Julia must work even with very limited JVMs (see section 2), without application specific class loaders, and without the
Java reflection API, the generators that generate the previous classes must be
useable either dynamically or statically, before launching the application. A
convention is therefore needed to name the generated classes. Indeed, in the
static case, the "generated" classes must be loaded from the classpath and, to
this end, their name must be known. And, therefore, the static class generator
cannot give arbitrary names to the class it generates.
A solution to this problem is to name the generated classes with the
parameters that were used to generate them. Indeed, in this case, when a
generated class is needed at runtime, and since the parameters to generate it
are of course known at this time, the class can either be dynamically generated
with these parameters or, in the static case, loaded with Class.forName
method, by using these parameters as a class name.
The problem of this solution is that the parameters used to generate a class
are quite long, and this would result in very long, and may be too long,
generated class names (some JVMs do not accept classes with a name of more than
256 characters). In order to solve this problem, the name of a generated class
is the hashcode of the parameters that were used to generate this class,
instead of being a bijective encoding of these parameters. Of course, two
different sets of parameters can have the same hashcode, and so conflicts are
possible. To solve this new problem, each generated class contains a
getFcGeneratorParameters method, defined in the Generated
interface, whose role it to return the exact parameters that were used to
generate the class. The algorithm to load a generated class is then the
following:
hashcode = hashcode(parameters needed to generate the desired class);
int n = 0;
while (true) {
Class c = Class.forName("package." + hashcode + "_" + n);
Generated g = (Generated)c.newInstance();
if (g.getFcGeneratorParameters().equals(above parameters)) {
return c;
}
++n;
}
Notes:
- the equals test in the above algorithm can take a long time with
very long parameters (typically 3000 characters for factory class generator
parameters). In order to speed up class loading, this check can be skipped. In
fact it is skipped by default, and executed only when the
julia.loader.check system property is set to "true".
- these naming conventions have some problems. Firstly, the generated names
are completely meaningless for humans, which makes debugging more difficult
(these names appear in stack traces). Secondly, because of conflicts, classes
may have different names on different machines, if they are not generated in
the same order. This may be a problem for Java's serialization mechanism.
8 Support for Constrained Environments
As explained in section 2, one of the goals of Julia is to be
usable even with very constrained JVMs and JDKs, such as the KVM and the J2ME
librairies (CLDC profile). This goal is achieved thanks to the following
properties:
- the size of Julia runtime (35KB, plus 10KB for the Fractal API), which is
the only part of Julia that is needed at runtime, is compatible with the
capabilities of most constrained environments;
- Julia can be used in environments that do not provide the Java Reflection
API or the ClassLoader class, which are needed to dynamically
generate the Julia application specific classes, since these classes can also
be generated statically, in a less constrained environment;
- the Julia classes that are needed at runtime, or whose code can be copied
into application specific runtime classes, use only the J2ME, CLDC profile
APIs, with only two exceptions for collections and serialization. For
collections a subset of the JDK 1.2 collection API is used. This API is not
available in the CLDC profile, but a bytecode modification tool is provided
with Julia to convert classes that use this subset into classes that use the
CLDC APIs instead. Likewise, for serialization, which is not supported in
CLDC, a bytecode modification tool is provided to remove serialization related
code in Julia. In other words the Julia jars cannot be used directly with
CLDC, but can be transformed automatically in new jars that are compatible
with this API.
A Mixins reference
A.1 Mixins for the Component interface
Figure 2: mixins for the Component interface
Julia provides two mixins for the Component interface:
- BasicComponentMixin provides a basic implementation, that does
not depend on the component's type system;
- TypeComponentMixin extends an existing Component
implementation in order to support the basic type system (this mixin adds some
type related checks, and provides support for the lazy creation of interfaces
for collection interface types).
The first mixin requires a base class that implements the Controller
interface, such as the BasicControllerMixin (this class is an empty
implementation of the Controller interface). The second mixin requires
a base class that implements the Component interface. These
dependencies are summarized in the figure below (an arrow from a mixin A to a
mixin B means that A needs a base class that provides the same fields and
methods as B - it does not mean that A requires exactly the B mixin).
A.2 Mixins for the TypeFactory interface
Figure 3: mixins for the TypeFactory interface
Julia provides two mixins for the TypeFactory interface:
- BasicTypeFactoryMixin provides a basic implementation;
- CheckTypeFactoryMixin extends an existing TypeFactory
implementation in order to perform some checks before creating interface
types. This mixin requires the Java Reflection API and the
ClassLoader class, and so can not be used with the CLDC profile.
A.3 Mixins for the GenericFactory interface
Figure 4: mixins for the GenericFactory interface
Julia provides two mixins for the GenericFactory interface:
- BasicGenericFactoryMixin provides a basic implementation. This
mixins requires a base class with a Loader field and a
TypeFactory field. These fields can be provided and initialized by
mixins such as the UseLoaderMixin and UseTypeFactoryMixin
mixins, which themselves require a base class that implements the
Controller interface;
- CheckGenericFactoryMixin extends an existing
GenericFactory implementation in order to perform some checks before
creating components. This mixin requires the Java Reflection API and the
ClassLoader class, and so can not be used with the CLDC profile.
A.4 Mixins for the Factory interface
Figure 5: mixins for the Factory interface
Julia provides four mixins for the Factory interface, or more precisely
for the Julia Template interface, i.e. for Factory
interfaces for Fractal template components:
- BasicTemplateMixin provides a basic implementation of the
Factory interface for template components;
- NameTemplateMixin extends an existing Factory
implementation in order to copy the name of the template to the components
instantiated from it;
- AttributeTemplateMixin extends an existing Factory
implementation in order to copy the attributes the template to the components
instantiated from it;
- SingletonTemplateMixin extends an existing Factory
implementation in order to give it a "singleton" semantics.
A.5 Mixins for the BindingController interface
Figure 6: mixins for the BindingController interface
Julia provides a lot of mixins for the BindingController interface.
First of all, it provides two mutually exclusive, basic implementations of the
BindingController interface:
- BasicBindingControllerMixin is based on a hash table;
- ContainerBindingControllerMixin delegates to the
BindingController interface of the content part of the component, for
"container" components;
Then four other mixins are provided, which extend any existing
BindingController implementation (such as the two previous mixins), in
order to perform various checks:
- CheckBindingMixin performs some basic checks before binding or
unbinding an interface (such as "interface already bound", or "interface not
bound"). These checks do not depend on any other concerns than the "binding"
concern;
- TypeBindingMixin performs type related checks, for the basic type
system. For example, this mixin checks the existence of the interface to be
bound or unbound, checks that a client interface is always bound to a server
interface (whose type is compatible with the client interface type), and so
on;
- LifeCycleBindingMixin performs life cycle related checks before
unbinding an interface (the component must be stopped for doing that);
- ContentBindingMixin performs structural constraints related
checks before binding an interface (all bindings must be local bindings).
Two other mixins are designed to extend the
ContainerBindingControllerMixin:
- OptimizedContainerBindingMixin extends an existing
BindingController implementation in order to skip Interface
objects, which removes one indirection for each binding of the component (the
bindFc method of this mixin stores the server interface argument in
an internal hash table, and calls the overriden method with the "impl" link of
this server interface as argument, instead of the server interface itself; the
other methods are modified accordingly).
- InterceporBindingMixin extends an existing
BindingController implementation in order to properly manage output
interceptors.
And, finally, two other, mutually exclusive mixins are designed to extend the
BasicBindingControllerMixin, and target composite components:
- CompositeBindingMixin extends an existing
BindingController implementation in order to update the "impl" links
(see section 4.1) of the Interface objects of the
component when bindings are changed.
- OptimizedCompositeBindingMixin does the same thing, but with an
optimized algorithm that can create and update shortcut links between
components (see appendix B).
A.6 Mixins for the ContentController interface
Figure 7: mixins for the ContentController interface
Julia provides six mixins for the ContentController interface:
- BasicContentControllerMixin provides a basic implementation, that
does not depend on the component's type system, or any other concern;
- CheckContentMixin extends an existing ContentController
implementation in order to perform some basic checks before adding or removing
a sub component (in particular to avoid cycles in the component's hierarchy).
These checks do not depend on any other concern than the "content"
concern;
- TypeContentMixin extends an existing ContentController
implementation in order to support the basic type system (this mixin adds some
type related checks, and provides support for the lazy creation of internal
interfaces, for collection interface types);
- BindingContentMixin extends an existing ContentController
implementation in order to check binding related constraints before removing a
component (removing a component must not create non local bindings);
- LifeCycleContentMixin extends an existing
ContentController implementation in order to check life cycle related
constraints before removing a component (a sub component can not be removed if
the super component is not stopped);
- SuperContentMixin extends an existing ContentController
implementation in order to notify the sub components when they are added or
removed from a super component, through the Julia
SuperControllerNotifier interface. This mixin should be put at the
end of the mixin list, in order to notify the sub components only after all
the checks have been passed.
A.7 Mixins for the SuperController interface
Figure 8: mixins for the SuperController interface
Julia provides only one mixin for the SuperController interface,
which provides a basic implementation of this interface.
A.8 Mixins for the LifeCycleController interface
Figure 9: mixins for the LifeCycleController interface
Julia provides two, mutually exclusive mixins that implement that implement the
LifeCycleController interface:
- BasicLifeCycleController provides an implementation that can work
with primitive or composite components, and which is based on a counter that
is updated by associated interceptor objects.
- OptimizedLifeCycleController provides an implementation that
works ony with composite components, and that does not requires associated
interceptors. This implementation works by stopping simultaneously all the
direct and indirect primitive sub components of the composite components.
These two mixins require methods that are provided by the
BasicLifeCycleCoordinator mixin. The TypeLifeCycleMixin
extends an existing LifeCycleController implementation in order to
check type related constraints before starting a component (more precisely, this
mixin checks that all the mandatory client interface of the component and of its
direct and indirect sub components ar bound). Finally the
ContainerLifeCycleMixin extends existing LifeCycleController
implementation in order to notify the component's content when it is started or
stopped, through its LifeCycleController interface, if it is present
(this mixin is intendend for "container" components).
A.9 Mixins for the NameController interface
Figure 10: mixins for the NameController interface
Julia provides only one mixin for the NameController interface, which
provides a basic implementation of this interface.
B Optimizations
B.1 Intra component optimizations
In order to (re)configure the controller objects that constitute the controller
part of a Fractal component, a natural solution would be to use Fractal itself:
each controller object would then be a Fractal component with, for example, a
BindingController interface to bind it to other controller objects. It
would also be possible, with this approach, to use composite components inside
the controller part of a component!
This solution will perhaps be implemented in future versions of Julia. The
current version uses a more efficient solution, in which it is always possible
to merge the controller objects into a single one (which is obviously not
possible with the above solution). This solution is the following:
- each controller object can provide and require zero or more Java
interfaces. The provided interfaces must be implemented by the object, and
there must be one field per required interface, whose name must begin with
weaveable for a mandatory interface, or weaveableOpt for an
optional interface (see below). Each controller class that require at least
one interface must also implement the Controller interface (see
below).
- in a given configuration, a given interface cannot be provided by more
than one object (except for the Controller interface). Otherwise it
would be impossible to merge these objects (an object cannot implement a given
interface in several ways).
- the bindings between objects in a given configuration are established
automatically in a two steps process:
- each controller object of the configuration is registered into
a "naming service". In practice, this naming service is the
InitializationContext interface;
- then, each controller object initializes itself by using the previous
"naming service" to retrieve the interface it requires.
To be more precise, lets suppose we have four control interfaces I, J, K and L,
and three controller classes IImpl, JImpl and KImpl.
These classes should look like this:
public class IImpl implements Controller, I {
// indicates a required interface of type J
public J weaveableJ;
// indicates an optional required interface of type L
public L weaveableOptL;
// other fields:
public int foo;
// implementation of the Controller interface
public void initFcController (InitializationContext ic) {
weaveableJ = (J)ic.getInterface("j");
weaveableOptL = (L)ic.getOptionalInterface("l");
}
// other methods:
public void foo (String name) {
weaveableJ.bar(weaveableOptL, foo, weaveableC.getFcInterface(name));
}
}
public class JImpl implements Controller, J {
public K weaveableK;
// implementation of the Controller interface ...
public void initFcController (InitializationContext ic) {
weaveableK = (K)ic.getInterface("k");
}
// other methods (not shown) ...
}
public class KImpl implements Controller, K {
// other methods ...
}
In the non optimized case, a component with these three controller objects is
instantiated in the following steps:
- an instance of IImpl, JImpl and KImpl is
created,
- the resulting objects are put in an InitializationContext
object,
- the initFcController method is called on each controller object
with this context as argument.
In the optimized case, the instantiation process is the following:
- the class obtained by "merging" the IImpl, JImpl and
KImpl is dynamically generated, or loaded from the classpath if it
has been statically generated before launching the application, or just
returned if it has already been generated or loaded.
- this class is instantiated.
The "merging" process is the following. Basically, all the methods and fields
of each class are copied into a new class (the resulting class does not depend
on the order into which the classes are copied). However the fields whose name
begins with weaveable are replaced by this, and those whose
name begins with weaveableOpt are replaced either by this, if
a class that implements the corresponding type is present in the list of the
classes to be merged, or null otherwise. Finally, the
initFcController methods from the Controller interface are
merged into a single initFcController method. The result is the
following class:
public class Cb234f2 implements Controller, I, J, K, ... {
// fields copied from the IImpl class:
public int foo;
// fields copied from the JImpl class: none
// fields copied from the KImpl class: none
// methods copied from IImpl:
public void foo (String name) {
bar(null, foo, getFcInterface(name);
}
// methods copied from JImpl (not shown) ...
// methods copied from KImpl (not shown) ...
// merged initFcController method
public void initFcController (InitializationContext ic) {
(J)ic.getInterface("j");
(L)ic.getOptionalInterface("l");
(K)ic.getInterface("k");
}
}
Notes:
- As explained in section 4.1, the controller part of a
component is made of controller objects and of interceptor objects. The above
optimization only applies to controller objects. Therefore, even with this
optimization, the controller part of a component is still made, in general, of
several Java objects. However, if the interceptor objects all delegate to the
same "content" object, and if they do not have conflicting interfaces, it is
possible to really have only one Java object for the whole controller part
of the component. In this case, which happens for most primitive components,
the instantiation process is the following:
- a class that merges the controller classes is generated or loaded as
before,
- a sub class of this class that implements the interception code for each
method of each functional interface is generated or loaded,
- this sub class is instantiated.
- Even if the controllers and interceptors are merged into a single object,
the content part of the component is still made of a separate object. It is
however possible to instantiate a whole component (i.e. the controllers, the
interceptors and the content part) as a single Java object. In order to do
this, the user component class is used as a super class to generate the merged
controller class, which is itself used a super class to generate the
interceptor class (as described above).
- In order to be able to dynamically deoptimize an optimized component, the
code generator that generates "merged" classes should also generate a
deoptimization method, i.e., instructions to create individual objects for
each original class, and to copy the fields of the merged class into the
appropriate objects. This is not yet implemented.
B.2 Inter component optimizations
In addition to the previous intra component optimizations, which are mainly
used to save memory, Julia also provides an inter component optimization, namely
an algorithm to create and update shortcut links between components, and
whose role is to improve time performances.
As explained in section 4.1, each interface of a component
contains an "impl" link to an object that really implements the component
interface. In the case of a server interface s, this link generally
points to an interceptor object, which itself points to the server interface to
which the complementary client interface of s is bound:
Figure 11: implementation of bindings with
CompositeBindingMixin
More precisely, this is the case with the CompositeBindingMixin (see
appendix A.5). With the
OptimizedCompositeBindingMixin, the "impl" links are optimized when
possible. For example, in the above figure, since I1 does not have an associated
interceptor object, and since component interface objects such as I2 just
forward any incoming method calls to the object referenced by their "impl" link,
the "impl" link of I1 can directly reference the interceptor associated to I2:
Figure 12: implementation of bindings with
OptimizedCompositeBindingMixin