-
Notifications
You must be signed in to change notification settings - Fork 5
Home
#Welcome to the Framework wiki!
###[Work in progress...]
- Getting started
- Setting up a project EMPTY
- Building and setting dependency
- A basic web application with Express
- Hello world
- The inversion of control EMPTY
- The Application context EMPTY 1. Initializing the application context
- Configuration class 1. Component scan 1. Property source 1. Importing configurations
- Component class 1. Profiles 1. Qualifiers 1. Lifecycle callbacks
- Dependency injection 1. Insert 1. Autowire 1. Value
- Environment 1. Activating a profile 1. Property stringification
- Definition Post processors
- Post processors
- Order
- The Web
- The Dispatcher EMPTY
- Controller class 1. Request mapping 1. View handling
- Interceptors
- Aspect Oriented Programming(AOP)
- Introduction
- AOP concepts
- Declaring an aspect 1. Before 1. After Returning 1. After Throwing 1. After 1. Around
- Data Access EMPTY
- Caching EMPTY
- Testing EMPTY
'Framework' is a new lightweight web framework for NodeJS inspired by Spring. It provides features like IoC (Inversion of control), declarative programming with decorators (a.k.a. Annotations from Java), AOP (Aspect oriented programming), synchronous looking control flow (with async-await or generators) and many more which simplifies the development time and makes your projects maintainable. Developers familiar with Java/Spring web programming will have easy time using 'Framework'. Being built on top of Express.js with TypeScript enables also developers familiar with Express.js or similar web frameworks to easily start using it. Let's try it now and see all this in action!
###Building and setting dependency
- git clone https://github.com/saskodh/framework.git // clone the project from GitHub
- cd framework // go inside the project folder
- npm install // install the dependencies
- npm run build // run the build (from shell, in windows cmd will fail)
We need to declare the Framework project as local npm dependency. We can do that with running the following commands:
- cd ./framework/dist/src
- npm link
- cd ./framework-showcase
- npm link @sklechko/framework
Set up a node project and:
- cd "path/yourProject"
- npm link @sklechko/framework
Or If you want to start off of the showcase project, here is how to start it:
- git clone https://github.com/saskodh/framework-showcase.git // clone the project from GitHub
- cd framework-showcase // go inside the project folder
- npm install
- typings install
- npm link @sklechko/framework
- npm run start
###A basic web application with Express
Using Express.js is the easiest way of setting up a node web application with Framework. You will need to make a @Configuration()
class with a @ComponentScan()
pointing to your source directory (not a super-directory of node_modules). Make ConfigurationClass.ts
with the following code:
// imports...
@ComponentScan(__dirname/..) // put the correct path, use __dirname
@Configuration()
class ConfigurationClass {}
And then make a main.ts
with the following code:
// imports...
async function() {
let appContext = new ApplicationContext(ConfigurationClass);
await applicationContext.start();
let app = express();
app.use(appContext.getRouter());
app.listen(8000);
}();
You can refer to the main and the WebAppInitializer of the showcase project.
###Hello world
Make Greetingscontroller.ts
with the following code:
// imports...
@Controller()
export class GreetingsController {
@RequestMapping({ path: '/', method: RequestMethod.GET })
async helloWorld() {
return {"message": "Hello world!"};
}
}
Note that the @Controller()
class is a component and it will be passed by the component scan that you set up in the configuration class.
Now just compile all, run main.js and open localhost:8000 in your browser.
Every Framework application requires an instance of ApplicationContext
to function.
All errors thrown in the ApplicationContext extend the
ApplicationContextError
.
The application context is initialized by its constructor and its start()
method.
The constructor does the following things:
- Initializes the dispatcher (which creates the Router)
- Gets the data out of the
@Configuration()
class - Initializes the
Environment
with all its different properties and profiles. - Loads the components with component scan.
applicationContext.start()
is an async method and it does the following things:
- Initializing the components
- Wiring the components
- Running post construction
- Running post processors
Framework uses configuration classes to configure the application context. It is done solely with decorators and JSON properties files. The constructor of ApplicationContext
takes one configuration class as parameter, but it is possible to have more with the @Import()
decorator.
All other configuration-related decorators need to be put above the
@Configuration()
decorator, or they will throwDecoratorUsageTypeError
.
In order to do the component scan on your @Component()
classes, you need to put the @ComponentScan(path)
decorator on the @Configuration()
class. path
is the root directory from witch to do the component scan. It scans recursively into sub-directories. The scanning itself takes place during the initialization of the application context.
Please use
__dirname
when you want to specify a relative path.
Make sure NOT to component scan over the Framework files, which usually happens when you component scan on your project root.
In order to import properties from a JSON file, you need to put the @Propertysource(path)
decorator on the @Configuration()
class. path
is the path to the JSON file. The properties get imported during the initialization of the application context and they end up as the Environment.applicationProperties
.
Throws
BadArgumentError
when the property source can't be loaded.
Please use
__dirname
when you want to specify a relative path.
Be careful of the order of your
@PropertySource()
and@Import()
decorators, as they are executed bottom-up and if you have properties with the same key, the topmost decorator overrides the rest.
For modularizing configurations the @Import()
decorator is used. It allows for loading components, propertySources and componentScans from other configuration classes. The arguments should be the other configuration classes.
Here is the example from the Frmework-showcase
@Import(ControllersConfig, RepositoriesConfig, ServicesConfig)
@PropertySource(__dirname+'/../resources/app.properties.json')
@Configuration()
export class AppConfig {}
Throws
DecoratorBadArgumentError
when an argument is not a configuration class.
The @Component()
decorator is similar to Spring's @Bean
annotation. It is put on classes to mark them as components, which are processed by the @ComponentScan()
and initialized and wired with applicationContext.start()
.
All other component-related decorators need to be put above the
@Component()
decorator, or they will throwDecoratorUsageTypeError
.
If the constructor of the component throws, the applicationContext wii throw
ComponentInitializationError
.
The @Profile()
decorator allows you to indicate that a component is eligible for registration when one or more specified profiles are active. If more then one profile is defined with @Profile()
, then the component will register when at least one of them is active. You can put "!" in front of the profile name to indicate that the component should be activated when that profile is not active. For example the following component
@Profile("dev", "mongo", "!pg")
@Component()
class MyComponent {...}
will be active if "dev" is active OR "mongo" is active OR "pg" is NOT active.
The @Qualifier()
decorator is used on @Component()
classes to set a token (a Symbol object) as an alias to that component, which is later used in the dependency injection. This is best used to set a someInterfaceToken
to the components that implement someInterface
, because interfaces are only present in TypeScript and are removed during transpiling to JS.
To interact with the container’s management of the bean lifecycle, you can use the and decorators, which are put on methods of the @Component()
class. The @PostConstruct()
decorator allows a component to perform initialization work after all necessary properties on the component have been set by the application context. The @PreDestroy()
allows a bean to get a callback when the application context containing it is destroyed. Example:
@Component()
class MyComponent {
@PostConstruct()
init() {...}
@PreDestroy()
destroy() {...}
}
Don't forget to do
applicationContext.registerExitHook()
if you want to use@PreDestroy()
.
They throw
DecoratorUsageError
when used multiple times on the same component class.
If the method itself throws, the applicationContext will throw a
PostConstructionError
orPreDestructionError
respectively.
Dependency injection (DI) is a process whereby objects define their dependencies, that is, the other objects they work with, only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse, hence the name Inversion of Control (IoC), of the bean itself controlling the instantiation or location of its dependencies on its own by using direct construction of classes, or the Service Locator pattern.
Code is cleaner with the DI principle and decoupling is more effective when objects are provided with their dependencies. The object does not look up its dependencies, and does not know the location or class of the dependencies. As such, your classes become easier to test, in particular when the dependencies are on interfaces or abstract base classes, which allow for stub or mock implementations to be used in unit tests.
###Insert
@Inject()
is the decorator mostly used for dependency injection. It must be put on a property in a class which is a @Component()
. The wiring can either be done in two ways
- By type-checking the class of the property. Keep in mind that interfaces are only present in TypeScript and are removed during transpiling to JS, so this way doesn't work for them.
- With a token (a Symbol object) as a parameter to the
@Inject()
, which is also provided to the@Component()
that needs to be injected with the@Qualifier()
decorator.
let token = Symbol('token');
@Qualifier(token)
@Component()
class MyClass {}
@Component()
class MyOtherClass {
@Inject()
propertyWiredByType: MyClass;
@Inject(token)
propertyWiredByToken;
//...
}
If @Inject()
is used on a property which is not an Array, then it might throw ComponentWiringError`with RootCause: "No such component registered", when no component is found, or "Ambiguous injection" when multiple are found.
If @Inject()
is used on a property which is an Array, then an array will be injected of all components that match the token (Symbol) passed to the @Inject()
. In this case a token must be passed to the decorator, as type-checking doesn't work. This is useful for example in the observer pattern.
let observerToken = Symbol('token');
interface IObserver {
notify();
}
@Qualifier(observerToken)
@Component()
class ConcreteObserverA implements IObserver{...}
// more observers
@Component()
class Subject {
@Inject(observerToken)
observers: Array<Observer>;
notifyObservers(){
for (let observer in observers)
observer.notify();
}
//...
}
###Autowire
@Autowire()
is just an alias of @Inject()
when it takes no parameters, so it can only be used with type-check wirings.
###Value
@Value()
is used to inject property values (from all kinds of property sources, according to their order) into properties of a @Component()
class. Keep in mind that the [properties are getting stringivfied] (#property-stringification). The key for the property needs to be passed to the decorator as an argument. If the property exists a string is injected (which for example can cast to number, depending on the property type), if it doesn't then the property is undefined.
Showcase example:
@Value('db.pg.database')
private database: string;
@Value('db.pg.port')
private port: number;
The property key for the first argument in
process.argv
isapplication.process.node
, and the key for the second isapplication.process.entryFile
.
##Environment
The Environment
is an abstraction integrated in the application context that models two key aspects of the application environment: profiles and properties. It is available via applicationContext.getEnvironment()
or
@Inject()
private env: Environment;
A profile is a named, logical group of component definitions to be registered with the container only if the given profile is active. Components may be assigned to a profile via annotations. The role of the Environment object with relation to profiles is in determining which profiles (if any) are currently active, and which profiles (if any) should be active by default.
Properties play an important role in almost all applications, and may originate from a variety of sources: properties files, node properties, system environment variables, process properties, decorators. The role of the Environment object with relation to properties is to provide the user with a convenient service interface for configuring property sources and resolving properties from them.
The order of the property sources from top to bottom priority is as follows:
- process properties (
process.argv
) - node properties (
process.execArgv
) - process environment properties (
process.env
) - application properties (propeties from
@PropertySource()
files) - default value (if passed to the
environment.getProperty()
) - undefined (when no property is present for the given hey)
###Activating a profile There are various ways of how to activate a profile:
- Crate/modify the property with the key
application.profiles.active
. Be careful properties with the same key override themselves according to the property order and between different source files. - Put the
@ActiveProfiles()
decorator on a@Configuration()
class (either the main config class or one that is directly/indirectly imported to it) - Use the
envirnoment.setActiveProfiles()
. Make sure to do it beforeapplicaitonContext.start()
is called.
The methods mentioned above are all used, i.e. they don't override each other.
###Property stringification
All properties in the Environment
are kept as Map<string, string>
and as such need to be stringified. The map key is the json name, or in the case of nested object, json names joined with ".". The map value is the json value or in the case of an array, the values inside the array joined with ",". For example the property from source file
{ "objectName": { "propertyName": [ "valueOne", "valueTwo"] } }
gets turned into the map with key objectName.propertyName
and value valueOne,valueTwo
.
In the case of the process properties (process.argv
) and node properties (process.execArgv
) the string is the key to the value "true", unless it contains "=" in which case in "foo=bar" "foo" is the key to the value "bar". The only exception is in the first two arguments of process.argv
, where the strings are the values to the keys application.process.node
and application.process.entryFile
.
If any of the post processors throw the applicationContext will throw
PostProcessError
.
###Component Definition Post Processors
A @ComponentDefinitionPostProcessor()
class is a @Component()
which must implement the IComponentDefinitionPostProcessor interface. The interface declares one method-postProcessDefinition()
, which is executed before any components are instantiated. The semantics of this interface are similar to those of the IComponentPostProcessor, with one major difference: ComponentDefinitionPostProcessor operates on the bean configuration metadata; that is, the ComponentDefinitionPostProcessor reads the configuration metadata and potentially changes it before the container instantiates any components other than ComponentDefinitionPostProcessors.
You can configure multiple ComponentDefinitionPostProcessors, and you can control the order in which these ComponentDefinitionPostProcessors execute by annotating them with the @Order()
decorator. The ComponentDefinitionPostProcessor with the lowest value specified int he @Order decorator will execute first.
Example usage of @ComponentDefinitionPostProcessor
A @ComponentPostProcessor()
class is a @Component()
which can be implemented to provide your own (or override the container’s default) instantiation logic, dependency-resolution logic, and so forth. If a class is annotated with @ComponentPostProcessor()
it must also implement the IComponentPostProcessor
interface which declares two methods: postProcessBeforeInit()
and postProcessAfterInit()
. The postProcessBeforeInit()
method is executed after all the components are instantiated and wired, but before any of the hook methods (executePostConstruction()
) are applied. This means that before that method starts executing all the dependencies will be resolved. The other method declared with the interface-postProcessAfterInit()
will execute after the hook methods are applied.
You can configure multiple ComponentPostProcessor, and you can control the order in which these ComponentPostProcessorexecute by annotating them with the @Order()
decorator. The ComponentPostProcessor with the lowest value specified int he @Order decorator will execute first.
##Order
The @Order()
decorator is used to define the sort order for an annotated component. Lower values have higher priority. When sorting, the components that are not annotated with @Order()
will get that highest value.
##Introduction Aspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns such as transaction management that cut across multiple types and objects. (Such concerns are often termed crosscutting concerns in AOP literature.) ##AOP concepts
- Aspect: a modularization of a concern that cuts across multiple classes. Aspects are implemented using regular classes annotated with the
@Aspect()
annotation. - Join point: a point during the execution of a program, such as the execution of a method or the handling of an exception.
- Advice: action taken by an aspect at a particular join point. Different types of advice include "around," "before" and "after" advice.
- Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).
- After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.
- After throwing advice: Advice to be executed if a method exits by throwing an exception.
- After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).
- Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
Any class defined in your application context that is annotated with the @Aspect()
annotation will be automatically detected and used to configure AOP. The following example shows the minimal definition required for a not-very-useful aspect
import ...
@Aspect()
public class NotVeryUsefulAspect {
}
Before advice is declared in an aspect using the @Before annotation which takes one argument; An object with two properties. The classRegex property which matches the given regex with all the components in the application context, and the methodRegex property which to matches the regex with all the methods from the previously matched class.
import ...
@Aspect
public class BeforeExample {
@Before({ classRegex: 'someClass', methodRegex: 'someMethod'})
public void doBefore() {
// ...
}
}
If the advice itself throws, the proxy will throw
BeforeAdviceError
.
After returning advice is declared in an aspect using the @AfterReturning annotation. The first parameter of the after returning will be the result that was returned from the execution of the pointcut method.
import ...
@Aspect
public class AfterReturningExample {
@AfterReturning ({ classRegex: 'someClass', methodRegex: 'someMethod'})
public void doAfterReturning() {
// ...
}
}
If the advice itself throws, the proxy will throw
AfterReturningAdviceError
.
After throwing advice is declared in an aspect using the @AfterThrowing annotation. The first parameter of the after throwing advice must be of type Error and the value will be the error that was thrown while executing the pointcut method.
import ...
@Aspect
public class AfterThrowingExample {
@AfterThrowing ({ classRegex: 'someClass', methodRegex: 'someMethod'})
public void doAfterThrowing() {
// ...
}
}
After advice is declared in an aspect using the @After annotation. The first parameter of the after throwing advice will be the error that was thrown while executing the pointcut method, or the result that was returned from the pointcut method if it executed without throwing any error.
import ...
@Aspect
public class AfterExample {
@After ({ classRegex: 'someClass', methodRegex: 'someMethod'})
public void doAfter() {
// ...
}
}
If the advice itself throws, the proxy will throw
AfterAdviceError
, unless the original method threw, in which case this error only gets logged.
The final kind of advice is around advice. Around advice runs "around" a matched method execution. It has the opportunity to do work both before and after the method executes, and to determine when, how, and even if, the method actually gets to execute at all. Around advice is often used if you need to share state before and after a method execution. Always use the least powerful form of advice that meets your requirements (i.e. don’t use around advice if simple before advice would do).
Around advice is declared using the @Around() decorator. The first parameter of the advice method must be of type ProceedingJoinPoint. Within the body of the advice, calling proceed() on the ProceedingJoinPoint causes the underlying method to execute. After advice is declared in an aspect using the @Around annotation.
import ...
@Aspect
public class AroundExample {
@Around ({ classRegex: 'someClass', methodRegex: 'someMethod'})
public void doAround(proceedingJoinPoint) {
// doSomethingBefore...
proceedingJoinPoint.proceed();
// doSomethingAfter...
}
}
Framework projects are usually based on the MVC pattern and use Express.js as a webApp, although the user can choose to use something else. Framework exposes an express router (applicationContext.getRouter()
) which the user can integrate into his application. From the showcase:
this.app = express();
//...
if (applicationContext) {
await applicationContext.start();
initializer.getApplication().use(applicationContext.getRouter());
}
//...
initializer.getApplication().listen(this.PORT, function () {
resolve(true);
});
If an unhandled error is thrown somewhere in the stack, the dispatcher will return response with status 500 and appropriate message.
##Controller class
A @Controller()
class is a @Component()
which handles the HTTP requests, according to the MVC pattern.
###Request mapping
The @RequestMapping()
decorator can be put either on a @Controller()
class or on its methods. When it is put on the class it is used to set the prefix for the paths on all RequestMappings on its methods. When it is put on a method it registers the method to the specified path and RequestMethod
Here is the example from the Frmework-showcase
@RequestMapping({ path: '/todos' })
@Controller()
export class TodoController {
//...
@RequestMapping({ path: '/getAll', method: RequestMethod.GET })
async getAllTodos() {
return await this.todoService.getAll();
}
@RequestMapping({ path: '/:id', method: RequestMethod.GET})
async getTodo(request: Request) {
let id = request.params.id;
return await this.todoService.get(id);
}
//...
}
Notice the extraction of arguments from the URL path in the example.
If the method throws on a request, the
next()
method will be called withRouteHandlerError
.
###View handling
The view handling is done with the @View()
decorator. @View()
can only be used above @RequestMapping()
and it forces the dispatcher to do response.render(viewName, methodResult)
instead of response.json(methodResult)
. viewName
is the argument passed to the @View()
decorator, if nothing is passed, then the method name is used.
Here is the example from the Frmework-showcase
@View("sayHi")
@RequestMapping({ path: '/sayHello/:name', method: RequestMethod.GET })
async sayHello (request: Request) {...}
@View()
@RequestMapping({ path: '/hi', method: RequestMethod.GET })
async sayHi() {...}
Keep in mind you need to set up the view-handling properties of the application yourself. showcase example:
//view setup
this.app.set('views', path.join(__dirname, '../views'));
this.app.set('view engine', 'ejs');
##Interceptors
An @Interceptor()
class is a @Component()
which has methods for intercepting a HTTP request. The Interceptor
interface defines the three methods (preHandle(...)
, postHandle(...)
, afterCompletion(...)
), but it doesn't have to be implemented, as the interceptor might only use some methods. Although it is recommended to use at least one method from the Interceptor
interface on classes decorated with @Interceptor()
.
preHandle(request, response)
is called before the actual handler is executed.
When this method returns true or undefined, the handler execution chain will continue; when it returns false, then the Dispatcher assumes the interceptor itself has taken care of requests (and, for example, rendered an appropriate view) and does not continue executing the other interceptors and the actual handler in the execution chain.
postHandle(request, response)
is called after the actual handler is executed.
afterCompletion(request, response)
is called after the complete request has finished.
// TODO: change link URL one interceptors are merged in master
Here is the example from the Frmework-showcase
@Interceptor()
export class TestInterceptor implements Interceptor {
preHandle(request, response) {
if(request.originalUrl === '/hi'){
response.preHandleProperty = 'interceptor preHandling works!';
}
}
postHandle(request, response) {
if(request.originalUrl === '/hi'){
response.$$frameworkData.model.postHandleProperty = 'interceptor postHandling works!';
}
}
afterCompletion(request, response) {
if(request.originalUrl === '/hi'){
console.log('interceptor afterCompletion works!');
}
}
}
If
preHandle()
orpostHandle()
throws,next()
will be called withInterceptorError
. IfafterCompletion()
throws anInterceptorError
will be thrown, which gets nowhere because it is on an event listener method, but otherafterCompletion()
won't get executed.
##Caching