Axon Framework - DDD and EDA meet together

02 Aug 2011 ·

CQRS (Command Query Responsibility Segregation) is a new approach towards building scalable and distributed systems that is based on simple pattern know as Command Query Separation (CQS). In short, you should design your system in a way that it either processes a command or serves response to a query. CQRS in its core is quite simple. Just split your service interface into two parts: Query Service and Command Service and you are done. But what is important is that by making this simple separation, you make your system open to many opportunities for architecture that may otherwise not exist. In this article I will not show you what is possible when fully applying CQRS (check this CQRS Starting Page for details). Instead I will present you how some design patterns can effectively be applied to standard ORM-based application when you decide to split your interface into Commands and Queries and use Axon - CQRS framework for Java. But first we must talk about DDD as it plays crucial role in building scalable and maintainable systems and fits very well to CQRS-based architecture.

DDD shortly

Generally DDD is about domain model that, when expressed in Ubiquitous Language, can be shared by developers and domain experts allowing them to communicate effectively. From technical point of view DDD is about modeling your core domain using aggregate roots (AR), entities, value objects and some other artifacts like domain services and repositories. What is important, when applying DDD to medium or large domains (generally complex domains are good candidates for DDD), you should not be expecting one model to arise representing all areas covered by the system. Each area will likely be modeled separately (inside its own Bounded Context. This aspect is very important, especially if your application starts to grow covering more and more business activities. If you stick with one model aka EDM (Enterprise Domain Model) (that typically reassembles model of relations inside your sql database), you will end up with monolithic system not capable of adjusting to business requirements. Therefore building application DDD-way should be seen as completely different approach comparing to standard approach (one model to rule them all approach). You will need additional means to express communication between your different models (Bounded Contexts). Thats where EDA - event based communication comes in.

Let's see how to apply DDD and EDA to standard ORM-based application with use of Axon.

Axon introduction

Axon framework provides building blocks for CQRS applications. Some blocks, i.e. event sourcing, asynchronous processing of commands (no response from Command Handler)) are optional and can be omitted. If you are not ready for (or just don't need) full CQRS, you can still benefit from other goodies such as synchronous command handling, events, sagas and others. Additionally Axon integrates deeply with Spring making configuration a trivial task (just use built in namespaces (xml part) and annotations). Unique feature of Axon is that it allows to integrate existing JPA-based application. To make your entities become Aggregate Roots extend AbstractAggregateRoot class, to load your ARs from database use GenericJpaRepository (or even better HybridJpaRepository if you want to use EventStore). For detailed instructions see User's Guide. Lets start working with code.

Get rid of Trasaction Script

When applying DDD, we tend to build rich entities that encapsulate behavior. In contrast to standard approach with anemic domain model and procedural code inside Application Services (see: Transaction Script), most of business logic should be handled inside Aggregate Roots. When a command comes in, it is dispatched to Command Handler whose only job should be to get AR from Repository and invoke single method on it.

Lets imagine a system that manages user accounts (Account AR). One of Account's properties is state. To activate an account, AccountActivateCommand must be sent by the client. In order to handle this command, we must register and implement specialized Command Handler:

class AccountCommandHandler {
    @CommandHandler
    public Account activate(AccountActivateCommand command) {
        Account account = accountRepository.findById(command.getAggregateId());
        account.activate();
        return account;
    }
}

That's it. Nothing special. Clean code so far.

Implementing business logic - uniformed approach

First, we need to extend our model in order to make it more interesting :) Our system must support handling user's payments related to Payment Period. The following requirements are added:

1) when Account is activated, Payment Period must be created that will be used to keep track of payments related to the Account

2) after Payment Period expires, subsequent Payment Period must be created, but expired Payment Period should still be accessible

We could model our Account like this:

class Account extends AbstractAggregateRoot {
    {...}
    @OneToMany
    private List<PaymentPeriod> paymentPeriods;

    private int currentPaymentPeriod;
    {...}
}

class PaymentPeriod {

  @ManyToOne
  private Account account;
  {...}
}

In this model, Account is responsible for managing Payment Periods. It sounds good, we can implement account activation inside Account class:

 
public void activate() {
  validateTransitionToStatus(ACTIVE)

  apply(new AccountActivatedEvent())

  // handle AccountActivatedEvent
  status = ACTIVE
  currentPaymentPeriod++;
  paymentPeriods.add(new PaymentPeriod(...))
}

Please notice three blocks of code in this method and theirs order. This separation is dictated by CQRS-based design. You should have all these blocks in every method that is invoked by Command Handler. The blocks are:

1) validation - checking if operation is allowed and will not break consistency of the Aggregate

2) raising event - creating a Domain Event containing information about Aggregate's change

3) handling event - updating state of the Aggregate (no exceptions, no logic here!)

Every change of Aggregate's state must be signaled with an Event (Domain Event). All Domain Events will be stored in Event Store (if configured) (just serialized as Blobs or Clobs to single table).

If our Aggregate Root is event-sourced (is reconstructed from Event Store instead or being created by EntityManager), we should move event handling code to separate method:

@EventHandler
private void accountActivated(AccountActivatedEvent event) {
  status = ACTIVE
  currentPaymentPeriod++;
}

We will not discuss Event Sourcing in this article, as we don't want to get rid of our powerful ORM, or do we? (actually in CQRS world ORM is persona non grata - you were warned ;)

Keep your ARs lously coupled

Going back to our model, after thinking a little bit longer, we realize that it doesn't fit well our needs... One of the requirements is to process commands related directly to Payment Periods. These commands should be dispatched directly to Payment Period entity rather than go through Account AR. Payment Period should be an AR on its own. When we think more about this (and talk with our Domain Expert (if we have one;)), the separation of Account and Payment Period will become even more obvious. We could easily imagine two services Account Service (responsible for accounts management) and Payment Service (responsible for payment registration) working independently.

So we can simplify Account AR and upgrade Payment Period to AR:

class Account extends AbstractAggregateRoot {
    {...}
    private int currentPaymentPeriod;

    public void activate() {
      validateTransitionToStatus(ACTIVE)

      apply(new AccountActivatedEvent())

      // handle AccountActivatedEvent
      status = ACTIVE
      currentPaymentPeriod++;
    }
    {...}
}

class PaymentPeriod extends AbstractAggregateRoot {
    {...}
    @ManyToOne
    private Account account;
    
    public PaymentPeriod(Account account) {
      apply(new PaymentPeriodCreated(account.getAggregateId()));
    }
    {...}
}

Now account activation is implemented partially (Payment Period is not being created). We could implement Application Service that would first call Account#activate and than create new Payment Period, but implementing business logic within Application Service layer leads to Transaction Script that we are trying to avoid (we want to avoid both ARs (Account and Payment Period) forcibly be invoked in the same transaction - it hurts system scalability). Lets think of our ARs on higher level. They belong to different contexts/services (virtual Account Service and Payment Service). The way to communicate between different contexts (services) is to use Domain Events!

Events to the rescue

We already have implemented raising of the AccountActivatedEvent in activate() method of Account AR. Now we must create Event Listener that will listen for this event and will send PaymentPeriodCreate command. With Axon it is as simple as creating new class:

 
@Component
public class PaymentService {

  @Autowired
  private CommandBus commandBus;

  @EventHandler
  public void createPaymentPeriodOnAccountActivation(AccountActivatedEvent event) {
    commandBus.dispatch(
      new PaymentPeriodCreateCommand(event.getAccountId())
    );
  }
}

Of course, we need to implement a command handler that will handle PaymentPeriodCreateCommand by creating Payment Period AR and adding it to Repository. Thats all. Now we have independent ARs that communicate with events. This design leads us in direction of autonomous components (services) communicating asynchronously in publish-subscribe model (aka: push integration model), possibly via some kind of EventBus or Broker (see: Avoid a Failed SOA: Business & Autonomous Components to the Rescue by Udi Dahan) But we will not go so far.

There are many other benefits from applying EDA. One of them is ability to keep detailed history of Events:

History of Events (Audit)

By storing Events in database (Event Store) we keep log of changes. We can easily create additional table containing following data:

  • aggregate root class
  • aggregate root id
  • event class
  • user

The table like this can serve basic reporting purposes. If we want to create more sophisticated reports in the future, we can reply events stored in Event Store and populate any report table we need. All history of changes is kept in Event Store.

Now lets see how we can model long running process with SAGA! In case you forgot the requirements, short reminder: new Payment Period must be created after the current one expires.

Enter SAGA

Saga is a stateful component (its state is persisted across invocations) that is capable of receiving events (including timeout events) (similar to Event Listener). Saga represents business process instance, in other words business process associated with particular AR(s).

 
class PaymentPeriodSaga extends AbstractSaga {

    @StartSaga
    @SagaEventHandler(associationProperty = "paymentPeriodId")
    public void paymentPeriodCreated(PaymentPeriodCreatedEvent event) {
        associateWith("accountId", event.getAccountId());
        getEventScheduler().schedule(
          // trigger datetime
          event.getValidityInterval().getEnd(), 
          // the event to publish
          new PaymentPeriodExpiredEvent(event.getContext(), event.getAggregateId())
            );
    }

    @SagaEventHandler(associationProperty = "paymentPeriodId")
    public void paymentPeriodExpired(PaymentPeriodExpiredEvent event) {
        RenewAccountCommand command = new RenewAccountCommand.Builder(event.getContext())
            .accountId(getAssociatedId("accountId"))
            .build();
        
        getCommandBus().dispatch(command);
    }
}

First method will be invoked on PaymentPeriodCreatedEvent and will result in creation of new Saga associated with Payment Period being created and related Account. Inside method body we schedule the PaymentPeriodExpiredEvent that will be triggered when validity interval of Payment Period ends (Axon provides Quartz-based implementation of Event Scheduler) . The second method is called when payment expiration happens (when PaymentPeriodExpiredEvent is triggered by the Event Scheduler). The only thing this method does is sending a command that will be processed by our Account Service (currentPaymentPeriod must be increased) and eventually by Payment Service (new Payment Period will be created).

Lets see how easy we can test our Saga with use of Test Fixture provided by Axon:

 
class PaymentPeriodSagaTest {
    AggregateIdentifier newPaymentPeriodId = ...
    AggregateIdentifier accountId = ...
    Interval validityInterval = new Interval(new DateTime(), new DateTime().plusDays(1));

    getFixture(PaymentPeriodSaga.class)
    // given
    .givenAggregate(newPaymentPeriodId)
      .published(new PaymentPeriodCreatedEvent(accountId, validityInterval))
    // when
    .whenTimeAdvancesTo(validityInterval.getEnd())
    // then
    .expectDispatchedCommandsEqualTo(
      new RenewPaymentPeriodCommand(accountId)
    );
}

Finally, I want to discuss one more topic related to DDD.

Don't pollute your core domain model

What is common mistake DDD beginners make is that they try to apply DDD totally (put all application logic into ARs boundaries). Let's take an example and add new requirement to our application: All entities (including Account) are separated by Sales Areas. Any operation on Account (creation, activation, etc.) can be performed only if the owning Sales Area is in status ACTIVE.

First, we will modify our model by adding the following JPA mapping to the Account AR:

 
class Account extends AbstractAggregateRoot {
    {...}
    @ManyToOne
    private SalesArea salesArea;
    private int currentPaymentPeriod;
    
    public void activate() {
    {...}
    }
}

Now lets think of the requirement. Where should we put the checking if the Sales Area is active? The activate() method of Account AR seems to be the perfect place. But if we think more, we realize that Sales Area does not belong to our core domain! Checking status of the Sales Area inside Account AR will pollute the code (checking must be done before any modification of Account's state). So our new model is broken! There should be no Account -> Sales Area mapping. But we can not remove it, because we reuse the same model for serving queries (we don't follow CQRS in this aspect) and we need to be able to filter Accounts by Sales Area easily. Ok, so the better place to put the checking would be a Command Handler (AccountCommandHandler). But it may be necessary to reuse this logic across different commands. What we need is some kind of interceptor that will prevent particular commands (account related or other) reaching Command Handlers. Not surprisingly Axon provides CommandHandlerInterceptor interface that allows for customized command handler invocation chains. No example this time, as it is quite easy to imagine:)

http://pkaczor.blogspot.com/2011/08/axon-framework-ddd-and-eda-meet.html

Autor: Paweł Kaczor

Paweł Kaczor

Programista, pasjonat, konsultant IT. Interesuje się nowoczesnymi framework-ami webowymi takimi jak: Seam, ZK. Obecnie w wolnym czasie poznaje język Scala.

Bullshit Bingo - REST i RESTful

25 Feb 2011 ·

Ostatnio nie mam trochę czasu na techniczne wpisy, także tym razem wpis będzie bardziej filozoficzny. Wczoraj zostałem zapytany o moją opinię na temat konwencji REST co zainspirowało mnie do stworzenia tego wpisu, ku przestrodze przed bezmyślnym wciskaniem tej architektury do każdej aplikacji.

REST w moim odczuciu urosło ostatnimi czasy do jednego z największych buzzwordów w świecie IT. Mało kto tak naprawdę rozumie tę architekturę, ale każda aplikacja, każdy serwis musi być REST-owy. Tak jakby od tego miało zależeć być albo nie być projektu. Być może w rozmowach biznesowych posiadanie REST-owej architektury jest na tyle przekonywującym argumentem, że łatwiej pozyskać sponsorów. Nie wiem. Ja jednak proponuję każdemu krytycznym okiem przyjrzeć się tej architekturze i samemu ocenić, czy w mojej aktualnej aplikacji jest ona w ogóle potrzebna? Czy nie jest tak, że generuje ona więcej problemów niż pożytku? Oto kilka problemów jakie można napotkać wdrażając architekturę REST-ową.

Id-ki, Id-ki widzę...

To co od razu rzuca się w oczy w architekturze REST-owej to wszędobylskie "Id-ki" w adresach URL. Pół biedy, jeżeli są to identyfikatory danych publicznych (np. ofert w sklepie), gorzej jak zaczynają się tam pojawiać identyfikatory zasobów, do których dostęp chcemy ograniczyć. A już szczytem masochizmu jest wrzucanie identyfikatorów profili użytkowników! Przykładowo edycja profilu użytkownika w naszej aplikacji może być dostępna pod takim URL-em:

/users/39/edit

No super! Piękny RESTful-owy URL. Z tym, że w kontrolerze dla takiego adresu i tak musimy zrobić walidację, że identyfikator użytkownika podanego w adresie URL jest taki sam jak identyfikator zalogowanego użytkownika! Inaczej mogli by oni edytować profile innych użytkowników. Także w tym przypadku użytkownik o identyfikatorze 39 i tak może wejść tylko na stronę podając parametr 39 w adresie URL, inaczej zostanie przekierowany na jakąś stronę informującą go, że nie ma dostępu do żądanego zasobu. Jaki zatem jest sens posiadania parametru w adresie URL, skoro i tak nie możemy go modyfikować? Nic nie zyskujemy, a możemy narazić się na kompromitację aplikacji gdy przez roztargnienie zapomnimy o walidacji albo ją przez przypadek wyłączymy. Czy nie lepiej byłoby mieć adres:

/users/edit

Albo jeszcze lepiej:

/edit_profile

I nie musieć martwić się o identyfikator podany w URL-u?

Sprawy tu mogą się jeszcze bardziej skomplikować. Wyobraźmy sobie, że chcemy użytkownikowi udostępnić stronę edycji zamówienia, które on złożył. Taka storna mogłaby być dostępna pod adresem:

/users/39/orders/10

W ten sposób sami sobie utrudniamy życie, bo nie dość, że musimy sprawdzać identyfikator użytkownika, to jeszcze musimy sprawdzić, że zamówienie o identyfikatorze 10 należy do użytkownika o identyfikatorze 39. W tej sytuacji o wiele lepsze jest wywalenie części dotyczącej użytkownika (która i tak jest stała):

/orders/10

Teraz sprawdzamy jedynie czy te zamówienie należy do zalogowanego użytkownika, którego identyfikator przechowujemy gdzieś w sesji (byle nie jawnie w pliku cookie ;)).

Także piersza zasada poprawnego implementowania REST-u mówi: wywal z URL-i stałe parametry, a w szczególności te, które dotyczą użytkowników.

No i warto zastanowić się nad jeszcze jedną kwestią. Czy te identyfikatory wnoszą cokolwiek użytecznego dla naszej aplikacji? Czy naprawdę uważamy, że użytkownicy będą przeglądać oferty poprzez modyfikowanie parametru ID w adresie URL? Moim zdaniem bardzo wątpliwe.

Ładne i brzydkie adresy URL

To co jest charakterystyczne w REST-owej architekturze to jej adresy URL tzw. pretty URL. No są one może i przyjazne, oraz nie kłują w oczy wszędobylskimi znakami ? oraz &. Problem pojawia się wtedy, kiedy nasza witryna dostaje się pod strzechy eskertów od SEO. Pierwsze co każą Ci zmienić w takiej aplikacji to właśnie adresy URL.

Stara zasada SEO mówi, że w adresie URL musi znaleźć się to do czego ta strona prowadzi. Przykładowo załóżmy, że poniższy adres prowadzi do strony oferty zakupu Mac Booka Pro (nie nie jestem funboyem Apple'a, nie mam ani Mac Booka, ani iPhone'a ani niczego innego z nadgryzionym jabłkiem w logo ;)):

/offers/131

Ekspert od SEO powie Ci "Panie, na taki URL to ty się nigdy nie wypromujesz, trzeba go będzie przepisać". No i z twojego pięknego URL-a robi się:

/oferty/131/Mac-Book-Pro

Także suma sumarum na końcu i tak będziesz musiał ręcznie te piękne URL-e przepisywać (albo zaimplementować sprytny sposób ich przepisywania coby nie robić tego ręcznie ;)) i nie będą one już takie piękne.

Pokaż mi swój identyfikator, a powiem ci jaką masz sprzedaż

Jest jeszcze jedna ciekawa osobliwość posiadania identyfikatorów w adresach URL. Otóż okazuje się, że mogą one udostępniać na zewnątrz informacje, którymi niekoniecznie chcielibyśmy się dzielić. Jeżeli przykładowo nasza strona pozwala podglądać publiczne profile użytkowników np. pod adresem:

/profiles/39

Inkrementując nasz identyfikator do momentu, aż otrzymamy 404 dowiemy się iluż zarejestrowanych użytkowników posiada dany portal (i autentycznie jest kilka takich portali gdzie to można zrobić, ale nie wymienię ich nazwy ;)).

W ten sposób możemy wyciągać z witryn informacje nie tylko o ilości zarejestrowanych użytkowników, ale i posiadanych ofert itd. Także trzeba być tego świadomym, że nie tylko to co jest na stronie jest informacją, ale i to co jest w adresie URL. I trzeba się zastanowić czy chcemy tę informacje upubliczniać. Może się skończyć na tym, że nasze REST-owe identyfikatory i tak będziemy zasłaniać jakimiś losowymi wartościami na podstawie których będziemy identyfikować zasób i w tym przypadku REST-owość może stać się bardziej uciążliwa niż pomocna.

Polimorfizm URL-i i metody HTTP

Konwencja REST-owa zbudowana jest na bazie czterech metod protokołu HTTP: GET, POST, PUT oraz DELETE. Już tutaj pojawia się pierwszy problem, gdyż przeglądarki internetowe z reguły wykorzystuję jedynie GET oraz POST. Stąd wszelkie formularze REST-owe do aktualizacji danych czy linki do ich usuwania muszą stosować różne sztuczki, aby zasymulować użycie metod PUT lub DELETE w żądaniach GET i POST. Czasami to stwarza problemy, zwłaszcza początkującym, przy konfiguracji routingów.

Konwencja ta niestety nie wyjaśnia do końca co powinno się dziać, jeżeli jakaś operacja się nie powiedzie. Przykładowo wchodzimy na ekran edycji złożenia zamówienia:

GET /orders/new

Konwencja REST mówi nam, iż utworzenie nowego zasobu (w tym wypadku o nazwie order) powinno zostać wykonane za pomocą metody POST ale pod adres kolekcji zasobów:

POST /orders

I taki adres będzie ukryty w naszym formularzu (w atrybucie action elementu form).

Takie działanie może i jest sensowne z punktu widzenia REST-a, ale z punktu widzenia działania witryny i przeglądarki internetowej jest wielce niepożadana. Dlaczego? Jeżeli nasz formularz jest nieprawidłowy nie chcemy lądować na stronie z listą, chcemy ponownie wyswietlić formularz wraz z odpowiednimi komunikatami o błędach. Innymi słowy nie chcemy być na /orders tylko ciągle na /orders/new. Niestety przeglądarka zmieni nam adres, mimo iż pozostajemy w tym samym miejscu. Jeszcze gorzej jest, jeżeli w ogóle nie udostępniamy strony o adresie /orders (czy chcemy użytkownikom udostępniać stronę z listą wszystkich zamówień złożonych w systemie?). Jeżeli użytkownik w tym momencie zrobi zakładkę, aby późiej powrócić i poprawić formularz zamiast formularza przywita go piękne 404, a przecież wcześniej był na tym formularzu i go wypełniał!

O tym problemie pisałem już kiedyś na moim blogu.

Parametrów kilka

Do tej pory skupiałem się na architekturze REST-owej w kontekście witryny internetowej. A co w przypadku serwisów webowych? Wydaje się, że architektura ta jest wręcz stworzona do takich serwisów. O ile człowiek nie będzie łaził po stronie manipulując identyfikatorami o tyle aplikacja korzystająca z serwisu nie będzie miała z tym problemu. Jednak w praktyce niekoniecznie wszystko wygląda tak różowo.

W całej swojej karierze deweloperskiej nie spotkałem jeszcze serwisu udostępniającego jakieś poważne usługi (np. płatności), którego żądania przyjmują tylko 1 parametr albo wcale. Zwykle takie żadania przyjmują kilka parametrów, a mi zdażało się pisać takie co przyjmowały 10-15 parametrów. I to wcale nie jest nic dziwnego! Oprócz samych parametrów żadania często wymagane są jakieś dane identyfikacyjne, jakieś tokeny, odciśki SSH czy dane statystyczne.

Wszystko to powoduje, że nasze piękne URL-e przestają być już piękne i nie ma w tym nic złego, w końcu te URL-e nie są przeznaczone dla ludzi. Maszyny, przynajmniej jak na razie, nie mają zmysłów ani gustów i im nie przeszkadzają takie długie adresy. W związku z tym jaki sens jest upierać się przy REST-owości w takich serwisach?

A morał jest krótki i niektórym znany...

Ok, czy to oznacza, iż architektura REST jest zła i nie powinno się z niej korzystać? Nie, absolutnie nie! Trzeba tylko umieć rozgraniczyć, kiedy architektura REST jest naprawdę potrzebna i sensowna, a kiedy jest tylko zwyczajnym buzzwordem. Trzeba być świadomym tego, co taka architektura ze sobą przynosi i czy w naszej aplikacji będzie ona miała zastosowanie i będzie w ogóle wykorzystywana.

Obecnie wszystkie projekty muszą być REST-owe, musi być JSON i musi być coś z Enterprise w nazwie. Ta buzzwordowa karuzela już bardziej działa na nerwy niż przynosi coś sensownego, ale co zrobić, prawa marketingu są nieubłagane i aby mieć inwestorów trzeba wykazać się innowacyjnością, nawet za cenę funkcjonalności, bezpieczeństwa czy łatwości programowania i utrzymania. Ja osobiście REST i RESTful dokładam do swojej listy Bullshit Bingo i Tobie też polecam zastanowienie się nad tym, czy faktycznie ten REST jest Ci potrzebny w Twojej aplikacji, aby potem na spotkaniu ktoś nie wstał i nie zawołał Bullshit!

http://michalorman.pl/blog/2011/02/bullshit-bingo-rest-i-restful/

Autor: Michał Orman

Programista, konsultant IT. Pasjonuje się nowoczesnymi technologiami i metodami wytwarzania oprogramowania, oraz pedantycznie dba o jakość kodu w swoich projektach. Obecnie poświęca swój czas na zgłębianie tajników korporacyjnej Javy EE oraz frameworka Ruby on Rails, a zawodowo zajmuje się szeroko pojętym konsultingiem.

JPA - stronicowanie wyników kwerendy

23 Feb 2011 ·

Interfejs kwerendy zdefiniowany w JPA (javax.persistence.Query) umożliwia stronicowanie listy wyników (paging). Służą do tego metody:

setFirstResult(int startPosition)
setMaxResults(int maxResult)

Wynikiem kwerendy ze stronicowaniem jest podzbiór obiektów czyli strona, której numer i rozmiar określają odpowiednio parametry startPosition i maxResult. Stronicowanie przyspiesza działanie aplikacji (mniejsza ilość danych do przetwarzania) oraz ułatwia użytkownikowi nawigację i wyszukiwanie określonych rekordów. Jednak jak to zwykle w świecie ORM bywa, każde rozwiązanie ma swoje "problemy", które w przypadku stronicowania objawiają się wraz z użyciem mechanizmu ładowania wyprzedzającego elementów kolekcji.

Ładowanie wyprzedzające ze stronicowaniem

Ładowanie wyprzedzające generalnie polega na wykonaniu kwerendy w taki sposób, aby razem z głównymi obiektami pobrane zostały obiekty powiązane (pojedyncze obiekty bądź kolekcje). Najbardziej popularnym sposobem ładowania wyprzedzającego jest złączenie tabel w kwerendzie (join fetching). Niestety, metoda ta nie nadaje się do zapytań ze stronicowaniem. Zobaczmy dlaczego.

Przykład

Mamy obiekt zamówienia (Order) zawierający pozycje (LineItem):

@Entity
public class Order {
  // ... ...
  @OneToMany
  private List<LineItem> lineItems;

 @Temporal(DATE)
 private Date orderDate;
}

Załóżmy, że chcemy wyświetlić użytkownikowi stronę z listą zamówień. Jednocześnie dla każdego zamówienia chcemy załadować jego pozycje. W tym celu tworzymy kwerendę na obiekcie Order, ze stronicowaniem oraz ze złączeniem kolekcji lineItems:

Query query = em.createQuery("SELECT o FROM Order o LEFT JOIN FETCH o.lineItems");
query.setFirstResult(1);
query.setMaxResults(3);
List<Order> page = query.getResultList();

Złączenie definiujemy w JPQL za pomocą klauzuli JOIN FETCH.

Oczekiwania wobec dostawcy JPA

Załóżmy, że w bazie mamy 3 zamówienia z różną ilością pozycji. Oczekujemy, że zarządca utrwalania (EntityManager) wykona pojedyncze zapytanie sql (z klauzulą left outer join) i zwróci listę zawierającą 3 obiekty Order. Dodatkowo oczekujemy, że w każdym obiekcie Order, kolekcja lineItems będzie załadowana.

Realizacja

  • Hibernate

Hibernate co prawda zwraca oczekiwany wynik, ale generuje podejrzanie brzmiący komunikat: firstResult/maxResults specified with collection fetch; applying in memory!

  • EclipseLink

EclipseLink zwraca 2 obiekty Order.

Wyjaśnienie

Aby zrozumieć co się stało, zobaczmy jak dokładnie wygląda wynik naszej kwerendy na poziomie rekordów bazy danych bez uwzględnienia stronicowania:

order_id line_id cust_name sku
1 1 Jan Kowalski 232342342
1 2 Jan Kowalski 345345443
2 3 Paweł Kaczor 655624323
3 4 Jerzy Dudek 673454345
3 5 Jerzy Dudek 563425676
3 6 Jerzy Dudek 234576854

Widzimy, że w wyniku złączenia, otrzymujemy zbiór rekordów liczebnością przekraczający ilość zamówień. Jeśli z takiego zbioru będziemy chcieli wyciągnąć stronę o rozmiarze 3, otrzymamy tylko pierwsze trzy rekordy zawierające zamówienia z id 1 i 2. Zamówienie z id 3 zostanie wykluczone. Otrzymamy zatem dwa zamówienia zamiast trzech! Wniosek: stronicowanie na poziomie rekordów w kwerendzie ze złączeniem outer join jest niedokładne.

Teraz już wiemy dlaczego EclipseLink zwrócił dwa zamówienia. Ale jakim sposobem Hibernate zwrócił trzy zamówienia? Odpowiedź jest prosta (ale bolesna). Hibernate omija problem poprzez załadowanie wszystkich rekordów z tabeli i wyselekcjonowanie strony w pamięci (stąd magiczne: "applying in memory"!). Rozwiązanie to zwiększa zużycie zasobów (procesora i pamięci), co przeczy głównemu celowi (zwiększeniu wydajności), dla którego stosujemy stronicowanie. Należy poszukać lepszego rozwiązania.

Ładowanie wsadowe ze stronicowaniem

Ładowanie wsadowe (batch fetching) to bardziej zaawansowany sposób pobierania wyprzedzającego. Obiekty powiązane nie są ładowane w kwerendzie głównej, ale w dodatkowej kwerendzie, której ostateczna postać zależy od wybranego (o ile dostawca na to pozwala) typu ładowania wsadowego.

Stronicowanie wykonywane jest bezproblemowo w kwerendzie głównej, która nie zawiera klauzuli outer join.

Konfiguracja - Hibernate

Dla kolekcji lineItems specyfikujemy ładowanie wsadowe używając adnotacji @org.hibernate.annotations.BatchSize:

@OneToMany
@BatchSize(size = 20)
private List<LineItem> lineItems;

Parametr size w adnotacji @BatchSize oznacza ilość elementów kolekcji, jaka zostanie załadowana w pojedynczej kwerendzie sql.

Konfiguracja - EclipseLink

Ten sam sposób ładowania wsadowego konfigurujemy w EclipseLink następująco:

@OneToMany
@BatchFetch(BatchFetchType.IN, size = 20)
private List<LineItem> lineItems;

Realizacja

Tworzymy kwerendę JPA tym razem bez klauzuli JOIN FETCH. W celu lepszego zobrazowania działania pobierania wsadowego, dodajemy warunek na pole orderDate.

Query query = em.createQuery("SELECT o FROM Order o WHERE o.orderDate = CURRENT_DATE");
query.setFirstResult(1);
query.setMaxResults(3);
List<Order> page = query.getResultList();

Wygenerowane zostają dwa zapytania sql:

  • kwerenda główna zamówień (ze stronicowaniem)
SELECT * 
FROM Order o
WHERE o.ORDER_DATE = ? LIMIT ? OFFSET ?
  • kwerenda dodatkowa - załadowanie wsadowe pozycji zamówień
SELECT *
FROM Order o, LineItem li 
WHERE o.ID = li.ORDER_ID AND li.ORDER_ID IN (?,?)

Jak widzimy, w celu załadowania pozycji tylko dla zamówień pobranych w kwerendzie głównej, w kwerendzie dodatkowej została użyta klauzula IN.

Optymalizacja - EclipseLink

EclipseLink pozwala skonfigurować trzy typy pobierania wsadowego: (IN, JOIN, EXISTS) Typ IN już znamy. Mankamentem jest tutaj ograniczona ilość elementów kolekcji, które mogą być załadowane w jednej kwerendzie sql. Efektywniejszym rozwiązaniem jest użycie kryteriów selekcji z kwerendy głównej w kwerendzie dodatkowej (typ JOIN - "The original query's selection criteria is joined with the batch query").

Konfiguracja:

@OneToMany
@BatchFetch(BatchFetchType.JOIN)
private List<LineItem> lineItems;

Wygenerowana kwerenda dodatkowa wygląda następująco:

SELECT *
FROM Order o1, Order o2, LineItem li 
WHERE o1.ID = li.order_id AND (o2.ID = li.order_id AND o1.ORDER_DATE = ?)

Widzimy, że problematyczna klauzula IN zastąpiona została kryterium wyboru identycznym jak w kwerendzie głównej.

Podsumowanie

W powyższym artykule przedstawiłem w jaki sposób stosować stronicowanie (paging) w kwerendach JPA. Szczegółowo omówiłem problem stronicowania, kiedy w kwerendzie używane jest ładowanie wyprzedzające elementów kolekcji.

http://pkaczor.blogspot.com/2011/02/jpa-stronicowanie-wynikow-kwerendy.html

Autor: Paweł Kaczor

Paweł Kaczor

Programista, pasjonat, konsultant IT. Interesuje się nowoczesnymi framework-ami webowymi takimi jak: Seam, ZK. Obecnie w wolnym czasie poznaje język Scala.

Mockowanie z użyciem Spring i Mockito

03 Jan 2011 · ·

W poniższym wpisie chciałbym przedstawić jak efektywnie skonfigurować testy integracyjne w Spring z użyciem mocków.

Testy integracyjne w Spring

Tworzenie i uruchamianie testów integracyjnych w Spring jest dziecinnie proste dzięki dobrodziejstwom jakie dostarcza Spring TestContext Framework. Zakładając, że plik xml konfiguracji kontekstu aplikacji Spring (Spring Application Context) w module aplikacji, który chcemy testować, nazwaliśmy applicationContext.xml, uruchomienie testu integracyjnego JUnit dla takiego moduły wygląda następująco:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:applicationConfig.xml" 
})
public class DoSthTest {

@Test
public void shouldDoSth() {}

}

Jest to najprostszy sposób na przetestowanie poprawności pliku konfiguracji kontekstu aplikacji, w którym zdefiniowane są zależności pomiędzy klasami zarządzanymi przez Spring.

No dobrze, ale chcemy przetestować konkretny serwis, który stworzyliśmy. Zatem do naszego testu wstrzykujemy serwis i tworzymy dla niego metodę testującą:

@Autowired
private DoSthService serviceToTest 

@Test
public void shouldReturnSth() {
    // when
    Object result = serviceToTest.doSth();
    
    // then
    assertNotNull(result)
}

Tworzenie mocka

Nasz serwis korzysta z kilku innych serwisów, w tym z serwisu HttpClient, który w naszym teście musimy zastąpić mockiem. Jednym ze sposobów jest ręczna zmiana zależności w kodzie testu:

@Test
public void shouldReturnSth() {
    // given
    HttpClient mockHttpClient = Mockito.mock(HttpClient.class);
    serviceToTest.setHttpClient(mockHttpClient);
    ...
}

Sposób ten ma jednak kilka wad:

  • Dla każdego testu (metody testującej) musimy sami tworzyć i przypisywać mocka

  • Serwis musi dostarczać settera dla zależności mockowanej, podczas gdy w przypadku wstrzykiwania zależności za pomocą adnotacji setter nie jest wymagany

  • Mockujemy tylko konkretną zależność między dwoma beanami. Jeżeli serwis który mockujemy jest używany przez inny serwis biorący udział w teście (np. DoSthService używa HttpClient i OtherService, a OtherService używa HttpClient) , musimy mockować każdą zależność osobno

Lepszym rozwiązaniem jest podmiana serwisu HttpClient bezpośrednio w kontekście aplikacji. Oczywiście nie możemy zmienić pliku applicationContext.xml, musimy stworzyć odrębny kontekst aplikacji i wskazać go w konfiguracji naszego testu:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:applicationConfig-test.xml" 
})
public class DoSthTest {

@Test
public void shouldReturnSth() {
    // when
    Object result = serviceToTest.doSth();
    ...
}
}

No dobrze, ale jak stworzyć mocka w pliku konfiguracji kontekstu aplikacji, skoro nasz mock nie jest utworzoną przez nas osobną klasą, ale został wygenerowany automatycznie przez framework Mockito ( Mockito.mock(HttpClient.class) ) ?

Rozwiązanie jest proste. Metodę Mockito.mock należy zadeklarować jako metodę fabrykującą naszego mocka podając jako argument tej metody klasę obiektu mockowanego:

<bean id="httpClient" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="org.apache.http.client.HttpClient"/> 
</bean>

Dostrajanie konfiguracji

Kopiowanie całej konfiguracji kontekstu aplikacji w celu nadpisania jednego serwisu prowadzi do redundancji kodu (kodu konfiguracji), a zatem zwiększa koszt utrzymania aplikacji. Jeżeli chcemy tego uniknąć, możemy zdefiniować dwa pliki konfiguracji kontekstu aplikacji w konfiguracji naszego testu:

@ContextConfiguration(locations = {
        "classpath:applicationConfig.xml", 
        "classpath:applicationConfig-test.xml" 
})
public class DoSthTest {
...
}

W pliku applicationConfig-test.xml definiujemy tylko mocki, które zastąpią istniejące beany w konfiguracji głównej. Zwróćmy uwagę na kolejność deklaracji plików konfiguracji. Ważne jest aby deklaracja pliku konfiguracji mocków następowała po deklaracji pliku głównego konfiguracji.

Autowstrzykiwanie mocków

W przypadku gdy korzystamy z autowstrzykiwania zależności, nasz mock będzie dostępny tylko gdy wstrzykiwanie zależności odbywa się na podstawie nazwy. A zatem mock nie zostanie znaleziony jeśli używamy adnotacji @Autowired, dla której wstrzykiwanie odbywa się na podstawie typu. Rozwiązaniem może być dodanie kwalifikatora wskazującego nazwę zależności:

@Autowired
@Qualifier(value="httpClient")
private HttpClient httpClient;

Jednak lepszym rozwiązaniem jest zastosowanie adnotacji @javax.annotation.Resource. W tym przypadku zależność najpierw wyszukiwana jest po nazwie, a w przypadku braku pasującej nazwy, po typie.

Włączanie/wyłączanie mocków

Czasami ten sam test integracyjny chcemy uruchamiać zarówno z serwisem w postaci mocka jak i z serwisem rzeczywistym. Jeśli stosujemy opisaną powyżej metodę nadpisywania beanów z konfiguracji głównej mockami z konfiguracji testowej, włączenie/wyłączenie mocka można łatwo osiągnąć modyfikując plik konfiguracji mocków (dodając bądź usuwając mocka). Jednak co z kodem sterującym zachowaniem mocka (patrz kod poniżej)?

@Autowired
private DoSthService serviceToTest 

@Resource
private HttpClient httpClient;

@Test
public void shouldReturnSth() {
    // given
    //httpClient must be a mock!
    given(httpClient.execute(any(HttpUriRequest.class)))
        .willReturn(myResponse);

    // when
    Object result = serviceToTest.doSth();
    ...
}

Uruchomienie tego kodu na obiekcie nie będącym mockiem zakończy się wyjątkiem. Należy zatem kod sterujący mockiem wykonać warunkowo tylko jeśli obiekt rzeczywiście jest mockiem. Sprawdzenie implementujemy następująco:

// given
if (!httpClient.getClass().isAssignableFrom(HttpClient.class)) {
    given(httpClient.execute(any(HttpUriRequest.class)))
        .willReturn(myResponse);
}

Współdzielenie mocków

Jeżeli chcielibyśmy użyć tego samego mocka w kilku metodach testowych musimy pamiętać o zresetowania stanu mocka przed każdym testem. Najprostszym rozwiązaniem jest dodanie do klasy testu metody resetMocks z adnotacją @org.junit.Before:

@Before
public void resetMocks() {
    Mockito.reset(httpClient);
}

Mockowanie częściowe

Na koniec przedstawię w jaki sposób utworzyć w konfiguracji kontekstu aplikacji częściowego mocka, czyli obiekt, którego zachowanie tylko w części chcemy mockować (np. zmieniając tylko rezultat wywołania metody):

  <bean id="httpClient" class="org.apache.http.client.HttpClient"/>
    <bean id="httpClientPartiallyMocked" class="org.mockito.Mockito" factory-method="spy">
        <constructor-arg ref="httpClient" /> 
    </bean>

W tym przypadku tworzymy mocka przy pomocy metody fabrykującej Mockito.spy, podając jako argument tej metody referencję do istniejącego bean-a.

http://pkaczor.blogspot.com/2010/12/mockowanie-przy-uzyciu-spring-i-mockito.html

Autor: Paweł Kaczor

Paweł Kaczor

Programista, pasjonat, konsultant IT. Interesuje się nowoczesnymi framework-ami webowymi takimi jak: Seam, ZK. Obecnie w wolnym czasie poznaje język Scala.

Spring DM - czyli Spring i OSGi

09 Dec 2010 · ·

Uff, ale dawno nie wrzuciłem żadnego posta na bloga. Czas nadrobić nieco zaległości, a ostatnie tygodnie stały u mnie pod znakiem Spring'a. Obecnie przyglądam się integracji tego frameworka z ciekawą technologią OSGi, która ostatnio staje się coraz bardziej modna. Spring w swojej rodzinie frameworków posiada jeden o nazwie Spring Dynamic Modules, który służy do integracji komponentów Spring'owych z platformą OSGi (i na odwrót).

Krótko o platformie OSGi

Jak dotąd platforma Java nie dorobiła się porządnego wsparcia dla modularnych aplikacji. Oczywiście istnieją wzorce pozwalające nam na modularyzację aplikacji, jednakże z punktu widzenia maszyny wirtualnej jest to wciąż jedna, monolityczna aplikacja. Czego zatem brakuje Javie, do pełnej modularyzacji?

  • Jasnej definicji tego, czym jest moduł.
  • Określania zakresu widoczności modułu dla innych modułów.
  • Określenia cyklu życia modułu.
  • Określenia sposobów interakcji modułów.

Jak łatwo się domyśleć technologia OSGi została opracowana w celu wypełnienia tej niszy i oferuje platformę, która zapewnia nam funkcjonalności, których brakuje w standardowej Javie.

To co oferuje nam OSGi to lekką platformę dla komponentowych, oraz zorientowanych na usługi aplikacji w ramach wirtualnej maszyny Javy (JVM). Możemy dynamicznie, w czasie działania aplikacji, modyfikować rejestr komponentów (bundle - w terminologii OSGi) dodając nowe i podmieniając istniejące komponenty. Platforma OSGi zapewnia nam pełną izolację komponentów, dzięki czemu mamy pewność, że nie będą one przeszkadzały sobie nawzajem (np. poprzez konflikty wersji zależnych bibliotek JAR).

Krótko o Spring DM

Spring Dynamic Modules łączy technologię Spring z platformą OSGi. Framework ten nie przynosi żadnej dodatkowej funkcjonalności dla OSGi a jedynie obserwuje rejestrowane komponenty tworząc dla nich Spring'owe konteksty aplkiacji. Framework ten potrafi udostępnić komponenty Spring'owe jako komponenty OSGi (widoczne dla innych komponentów OSGi), oraz dowolny komponent OSGi, nawet nie Spring'owy, zainstalować w kontenerze Springa.

Integracja Spring'a z OSGi

No dobra, starczy tej teorii, zobaczmy jak wygląda integracja tych platform w kodzie.

Będziemy potrzebować dwóch komponentów, z których jeden będzie dostarczał usługę, a drugi z niej korzystał. Oba komponenty utworzone zostaną jako osobne komponenty OSGi.

Najpierw musimy określić interfejs dla naszej usługi:

public interface AuthorizationService {
    boolean authorize(String username, String password);
}

Teraz możemy utworzyć trywialną implementację tego interfejsu:

public class DefaultAuthorizationService implements AuthorizationService {
    public boolean authorize(String username, String password) {
        return "foo".equals(username) && "secret".equals(password);
    }
}

Kolejny krok to utworzenie konfiguracji Spring'owej. Framework Spring DM wymaga, aby taka konfiguracja znajdowała się w katalogu META-INF/spring:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:osgi="http://www.springframework.org/schema/osgi"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.2.xsd">

    <context:component-scan base-package="demo.springdm" />

    <osgi:service interface="demo.springdm.api.AuthorizationService">
        <bean class="demo.springdm.api.impl.DefaultAuthorizationService" />
    </osgi:service>

</beans>

Najważniejszym elementem tej konfiguracji jest <osgi:service>, który deklaruje Springowy komponent jako komponent OSGi widoczny dla innych komponentów zainstalowanych w ramach tej platformy. Spring DM za nas zainstaluje komponent w rejestrze usług (ang. service registry) platformy OSGi przez co stanie się on dostępny dla innych komponentów.

Ok, moduł dostarczający usługe jest gotowy, teraz możemy przejść do modułu korzystającego z usługi. Utwórzmy zatem komponent Spring'owy do którego wstrzykniemy komponent zainstalowany w platformie OSGi:

@Component
public class AuthorizationClient {
    @Autowired
    private AuthorizationService authorizationService;

    @PostConstruct
    public void authorize() {
        System.out.println("Authorization for foo:bar : " + authorizationService.authorize("foo", "bar"));
        System.out.println("Authorization for foo:secret : " + authorizationService.authorize("foo", "secret"));
    }
}

Jak widać, jest to typowa klasa POJO z kilkoma adnotacjami typowymi dla Spring'a. Nic magicznego się tutaj nie dzieje. Aby kod ten działał potrzebujemy jednak komponentu authorizationService:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:osgi="http://www.springframework.org/schema/osgi"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.2.xsd">

    <context:component-scan base-package="demo.springdm" />

    <osgi:reference id="authorizationService" interface="demo.springdm.api.AuthorizationService" />

</beans>

W tym wypadku importujemy komponent OSGi jako komponent Spring'owy za pomocą <osgi:reference>.

Teraz pozostaje już tylko zainstalowanie komponentów w kontenerze OSGi (ja korzystam z Equinoxa):

$ java -jar org.eclipse.osgi_3.6.1.R36x_v20100806.jar -console

osgi> ss

Framework is launched.

id    State       Bundle
0     ACTIVE      org.eclipse.osgi_3.6.1.R36x_v20100806
1     ACTIVE      org.springframework.osgi.io_2.0.0.M1
2     ACTIVE      org.springframework.osgi.core_2.0.0.M1
3     ACTIVE      org.springframework.osgi.extender_2.0.0.M1
4     ACTIVE      org.springframework.aop_3.0.5.RELEASE
5     ACTIVE      org.springframework.asm_3.0.5.RELEASE
6     ACTIVE      org.springframework.beans_3.0.5.RELEASE
7     ACTIVE      org.springframework.context_3.0.5.RELEASE
8     ACTIVE      org.springframework.context.support_3.0.5.RELEASE
9     ACTIVE      org.springframework.core_3.0.5.RELEASE
10    ACTIVE      org.springframework.expression_3.0.5.RELEASE
11    ACTIVE      com.springsource.org.aopalliance_1.0.0
12    ACTIVE      com.springsource.net.sf.cglib_2.1.3
13    ACTIVE      com.springsource.slf4j.api_1.6.1
                  Fragments=15
14    ACTIVE      com.springsource.slf4j.org.apache.commons.logging_1.6.1
15    RESOLVED    com.springsource.slf4j.nop_1.6.1
                  Master=13
16    ACTIVE      com.springsource.ch.qos.logback.core_0.9.24
17    ACTIVE      com.springsource.ch.qos.logback.classic_0.9.24

osgi> install file:bundles/spring-demo/spring-dm-demo-producer-1.0.jar
Bundle id is 18

osgi> start 18 

osgi> install file:bundles/spring-demo/spring-dm-demo-consumer-1.0.jar
Bundle id is 19

osgi> start 19

osgi> Authorization for foo:bar : false
Authorization for foo:secret : true

osgi> ss

Framework is launched.

id    State       Bundle
0     ACTIVE      org.eclipse.osgi_3.6.1.R36x_v20100806
...
18    ACTIVE      demo.springdm.spring-dm-demo-producer_1.0.0
19    ACTIVE      demo.springdm.spring-dm-demo-consumer_1.0.0

Działa. Pierwsza integracja Spring'a z OSGi za pomocą frameworka Spring Dynamic Modules gotowa.

Podsumowanie

Platforma OSGi ma na celu wypełnienie luki jaka istnieje w Javie dotyczącej wsparcia dla aplikacji wielomodułowych. To co oferuje nam Java, czyli różne typy archiwów (JAR, WAR czy EAR) okazują się niewystarczające. OSGi jasno definiuje czym jest moduł, jaki jest jego cykl życia oraz izoluje moduły od siebie udostępniając tylko te usługi, które trzeba.

Spring Dynamic Modules to framework, który pozwala instalować komponenty Springo'we w kontenerze OSGi a także używać dostępnych komponentów i wstrzykiwać je do komponentów Springa. Całość dzieje się jedynie z pomocą odrobiny deklaracji w plikach XML-owych.

Działający kod aplikacji powstałej do tego posta można znaleźć pod adresem: https://github.com/michalorman/michalorman.github.com/tree/master/przyklady/spring-dm-czyli-spring-i-osgi

Znajduje się tam także Equinox skonfigurowany pod Spring DM 2.0.0.M1 oraz Spring'a 3.0.5.RELEASE (a nie 3.0.0.RC1 z jaką domyślnie jest Spring DM).

http://michalorman.pl/blog/2010/12/spring-dm-czyli-spring-i-osgi/

Autor: Michał Orman

Programista, konsultant IT. Pasjonuje się nowoczesnymi technologiami i metodami wytwarzania oprogramowania, oraz pedantycznie dba o jakość kodu w swoich projektach. Obecnie poświęca swój czas na zgłębianie tajników korporacyjnej Javy EE oraz frameworka Ruby on Rails, a zawodowo zajmuje się szeroko pojętym konsultingiem.