0.1.0-alpha-1 • Published 6 years ago

@molay/spring4js v0.1.0-alpha-1

Weekly downloads
2
License
CC-BY-NC-ND-4.0
Repository
-
Last release
6 years ago

Spring4js

The javascript version implementation of spring mvc.

Install

NOTICE: This library needs legacy decorator support!

yarn add @molay/spring4js
yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators babel-plugin-parameter-decorator

.babelrc

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    "babel-plugin-parameter-decorator",
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ]
  ]
}

Usage

Dependency Injection

import { SPRING } from '@molay/spring4js';
const { Container, Decorators } = SPRING;
const { Component, Autowired, Qualifier, Configuration, Bean, Lazy } = Decorators;

class Person {
  name;
  hp;
}

class Weapon {
  name;
  damage;
  attack(person) {
    person.hp -= this.damage;
  }
}

@Component()
class Gun extends Weapon {
  name = 'gun';
  damage = 1000;
}

@Lazy()
@Component({ name: 'rifle' })
class Rifle extends Weapon {
  name = 'rifle';
  damage = 2000;
}

@Component()
class Bomb extends Weapon {
  name = 'bomb';
  damage = 3000;
  constructor() {
    super();
    console.log('[Bomb created!]');
  }
}

@Component()
class Alice extends Person {
  name = 'Alice';
  hp = 1000;
  @Autowired({ type: Gun })
  weapon;
}

@Component()
class Bob extends Person {
  name = 'Bob';
  hp = 3000;
  @Autowired({ type: Bomb })
  weapon;
}

@Lazy()
@Component({ name: 'eve' })
class Eve extends Person {
  name = 'Eve';
  hp = 2000;
  weapon;
  @Autowired({ type: Weapon })
  @Qualifier({ name: 'rifle' })
  setWeapon(weapon) {
    this.weapon = weapon;
  }
}

@Lazy()
@Component()
class Scene {
  @Autowired({ type: Alice })
  alice;
  @Autowired({ type: Bob })
  bob;
  @Autowired({ type: Person })
  @Qualifier({ name: 'eve' })
  eve;

  @Autowired({ type: Object, required: false })
  @Qualifier({ name: 'not exists' })
  objectNotExists;

  play() {
    const { alice, bob, eve } = this;
    const persons = [alice, bob, eve];
    const numRounds = 10;
    for (let i = 0; i < numRounds && persons.length > 0; i++) {
      const person0 = persons[Math.floor(Math.random() * persons.length)];
      const person1 = persons[Math.floor(Math.random() * persons.length)];
      const message = [];
      message.push(`${person0.name} (HP ${person0.hp}) uses ${person0.weapon.name} (DAMAGE ${person0.weapon.damage}) to`);
      if (person0 !== person1) {
        message.push(`attack ${person1.name} (HP ${person1.hp}).`);
      }
      else {
        message.push(`suicide.`);
      }
      person0.weapon.attack(person1);
      if (person1.hp <= 0) {
        message.push(`${person1.name} has died.`);
        persons.splice(persons.indexOf(person1), 1);
      }
      else message.push(`${person1.name} has ${person1.hp} points hp remaining.`);
      console.log(`Round ${i + 1}: ${message.join(' ')}`);
    }
  }
}

const container = new Container().init();
const bean = container.getBean(Scene);
bean.play();

Spring MVC

import { SPRING, RequestMethod } from '@molay/spring4js';
const { Container, Decorators } = SPRING;
const { Controller, RequestMapping, AroundInvoke, Autowired, Configuration, Bean } = Decorators;

import Koa from 'koa';

// Around controller method and do sth
const CustomResponseProcess = function () {
  return AroundInvoke({
    method: function (bean, methodKey, args) {
      const a = Date.now();
      const returnValue = bean[methodKey].apply(bean, args);
      const t = Date.now() - a;
      const responseTime = `${t}ms`;
      const [ctx] = args;
      ctx.set('Content-Type', 'application/json');
      ctx.set('X-Response-Time', responseTime);
      return returnValue;
    }
  });
};

// Simulate bean object that needs to be asynchronously initialized
class ComplexBean {

  async _init01() {
    return new Promise(
      (value) => {
        const timeout = 100 + Math.random() * 900;
        setTimeout(() => {
          console.log(`[${new Date().toISOString()}] ComplexBean._init01 done after ${timeout.toFixed(3)}ms.`);
          value();
        }, timeout);
      },
      (reason) => {
      }
    );
  }

  async _init02() {
    return new Promise(
      (value) => {
        const timeout = 100 + Math.random() * 900;
        setTimeout(() => {
          console.log(`[${new Date().toISOString()}] ComplexBean._init02 done after ${timeout.toFixed(3)}ms.`);
          value();
        }, timeout);
      },
      (reason) => {
      }
    );
  }

  async init() {
    await this._init01();
    await this._init02();
  }
}

@Controller()
class Controller01 {
  @Autowired({ type: ComplexBean })
  complexBean;

  @CustomResponseProcess()
  @RequestMapping({ path: '/hello', method: RequestMethod.ALL })
  async hello(ctx) {
    ctx.status = 200;
    ctx.body = 'Hello World!';
  }

  @CustomResponseProcess()
  @RequestMapping({ path: '/echo', method: [RequestMethod.GET, RequestMethod.POST] })
  async echo(ctx) {
    ctx.status = 200;
    ctx.body = JSON.stringify(ctx);
  }

  @CustomResponseProcess()
  @RequestMapping({ path: '/test/:a?/:b?/:c?' })
  async test(ctx, @Autowired({ type: ComplexBean }) complexBean) {
    ctx.status = 200;
    ctx.body = JSON.stringify({
      flag: complexBean === this.complexBean,
      ...ctx.pathParams
    });
  }
}

@Controller()
@RequestMapping({ path: '/user' })
class Controller02 {
  @CustomResponseProcess()
  @RequestMapping({ path: '/', method: RequestMethod.POST })
  async create(ctx) {
    ctx.status = 200;
    ctx.body = JSON.stringify({
      status: 200,
      message: 'Create user succeed.'
    });
  }

  @CustomResponseProcess()
  @RequestMapping({ path: '/:id', method: RequestMethod.GET })
  async read(ctx) {
    ctx.status = 200;
    ctx.body = JSON.stringify({
      status: 200,
      data: {
      }
    });
  }

  @CustomResponseProcess()
  @RequestMapping({ path: '/:id', method: RequestMethod.PUT })
  async update(ctx) {
    ctx.status = 200;
    ctx.body = JSON.stringify({
      status: 200,
      message: 'Update user succeed.'
    });
  }

  @CustomResponseProcess()
  @RequestMapping({ path: '/:id', method: RequestMethod.DELETE })
  async delete(ctx) {
    ctx.status = 200;
    ctx.body = JSON.stringify({
      status: 200,
      message: 'Delete user succeed.'
    });
  }
}

// Custom configuration
@Configuration()
class Config {
  @Bean({ type: ComplexBean })
  getComplexBean(@Autowired({ type: Container }) container) {
    const bean = container.createBean(ComplexBean);
    bean.init();
    return bean;
  }
}

const app = new Koa();
app.listen(3000);

new Container()
  .setWebContext(app)
  .init();

Decorators

@Autowired

  • type: Function; Declares the annotated dependency type.
  • required?: boolean = true; Declares whether the annotated dependency is required.
@Component({ name: 'a' })
class ClassA {
}

@Component({ name: 'b' })
class ClassB {
}

class ClassC {
  @Autowired({ type: ClassA })
  objectA;

  // can not found matched bean, but no exception.
  @Autowired({ type: ClassB, require: false })
  @Qualifier({ name: 'b2' })
  objectB;

  // inject bean to parameter when method invoking
  // can not found matched bean, throw exception
  doSth(
    @Autowired({ type: ClassB })
    @Qualifier({ name: 'b2' })
    objectB2
  ) {
  }
}

@Qualifier

  • name?: string; Declares the name for looking up the annotated dependency.

@Configuration

class BeanA {
}

@Component()
class ComponentA {
  @Autowired({ type: BeanA })
  beanA;
}

@Configuration()
class Config {
  @Bean()
  getBeanA() {
    const bean = new BeanA();
    // do sth
    // ...
    return bean;
  }
}

@Bean

@Lazy

@Value

@Component

  • name?: string; Declares the bean name.

@Controller

  • name?: string; Declares the bean name.

@Service

  • name?: string; Declares the bean name.

@Repository

  • name?: string; Declares the bean name.

@RequestMapping

  • path: string | string[]; Declares the path mapping URIs.
  • method?: string | string[] = RequestMethod.ALL; Declares the HTTP request methods to map to.
  • params?: any; TODO
  • headers?: any; TODO
  • consumes?: any; TODO
  • produces?: any; TODO
@Component()
class UserDao {
}

@Controller()
@RequestMapping({ path: '/user' })
class UserController {
  @RequestMapping({ path: '/', method: RequestMethod.POST })
  async create(ctx, @Autowired({ type: UserDao }) dao) {
    // do sth
    // dao.create(...)
  }

  @RequestMapping({ path: '/:id', method: RequestMethod.GET })
  async read(ctx) {
  }

  @Autowired({ type: UserDao })
  dao;

  @RequestMapping({ path: '/:id', method: RequestMethod.PUT })
  async update(ctx) {
    // do sth
    // this.dao.update(...)
  }

  @RequestMapping({ path: '/:id', method: RequestMethod.DELETE })
  async delete(ctx) {
  }
}

@BeforeInvoke

  • method: (bean: any, methodKey: string | symbol, args: any[]) => void; The method will be called before the target method is called.

@AfterInvoke

  • method: (bean: any, methodKey: string | symbol, args: any[], returnValue: any) => void; The method will be called after the target method is called.

@AroundInvoke

  • method: (bean: any, methodKey: string | symbol, args: any[]) => any; The method will be called when the target method is called, you can customize the control logic of the target method. Need to pay attention to the return value.