Sunteți pe pagina 1din 116

EJB Design Patterns

EJB design patterns


• Best practice solution to commonly
reccuring problems.
• Enable us to design efficient, scalable and
maintainable systems
Benefits
• Provides a high-level language to discuss
design issues.
• Provides much of the design.
• Combinations of patterns form reusable
architectures.
Categories
• EJB Layer Architectural patterns:
Architecture or partitioning of logic
– Session Façade
– Message Façade
– EJB Command
– Data Transfer Object Factory
– Generic Attribute Access
– Business Interface
• Inter-tier Data Transfer Patterns: For
transferring data from the client to the
server and back.
– Data Transfer Object
• Domain DTO
• Custom DTO
– Data Transfer Hashmap
– Data Transfer Rowset
• Transaction and Persistence Patterns:
transaction control, persistence and
performance related
– Version Number
– JDBC for reading
– Data Access Command Bean/Data Access
Object
– Dual Persistent Entity Bean
• Client-side EJB Interaction Patterns:
maintainability and performance from client
point of view.
– EJB Home Factory
– Business Delegate
Session Façade
Background
• Fine-grained access through remote interfaces
increases network traffic and latency.
• Overhead of multiple network calls
• Processes involve multiple business objects.
• Data access and business logic scattered across
clients.
• Tight coupling between classes.
• Decrease in flexibility and design clarity.
• Transactions not safe: When calling an
entity beans methods directly, each method
call is a separate unit of work, and a
separate transaction.
• Transaction not maintained across methods
Need
• Invoke methods of multiple session or
entity beans in one transaction and one bulk
network call.
Solution 1
• Put extra logic into our entity beans to perform
many operations on behalf of a single client call.
• This solution introduces maintenance problems,
because our entity bean layer will likely be used in
many different ways over time.
• Merging our application logic (verbs) with our
persistence logic (nouns), which is poor
application design.
Solution 2
• Client demarcates a large transaction via the Java
Transaction API (JTA).
– High network overhead.
– Poor concurrency.
– High coupling.
– Poor reusability. Other clients cannot reuse
– Mixing of presentation logic with business logic.
– Poor maintainability. Usage of the Java Transaction
API
The Solution
• A server-side abstraction that serves as an
intermediary, and buffers calls to entity beans.
• Session beans will contain application logic to
fulfill use-cases.
• Each session bean performs bulk operations on
entity beans on behalf of a single client request.
• Clients have access to session beans only, not
entity beans.
Advantages
• Low network overhead.
• Lower overall overhead on client end.
• Session bean is the transactional façade, localizes
transactions to the server-side, and keeps them
short, giving higher concurrency.
• Lower coupling.
• Entity beans remain reusabe.
• Better maintainability: Clean separation of
middleware (transaction) and application logic.
• Clean verb-noun separation.
Role of Use Cases
• Do not map every use case to a session
façade.
• Consolidate them into fewer session beans
based on some logical partitioning.
• Design session facades to aggregate a group
of the related interactions into a single
session façade.
• For a banking application, group the
interactions related to managing an account.
• The use cases Create New Account, Change
Account Information, View Account
information, and so on all deal with the
coarse-grained entity object Account.
• Group into a single session façade called
AccountSessionFacade.
Message Façade
• For asynchronous use cases.
EJB Home Factory
try {
Properties properties = new Properties();
properties.put
(javax.naming.Context.PROVIDER_URl,
"some providers url");
properties.put
(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
"some name service");
initContext = new InitialContext(properties);
Object homeObject =
initContext.lookup("aHomeName");
myHome = (MyHome)
javax.rmi.PortableRemoteObject.narrow
(homeObject, MyHome.class );
} catch (Exception e) {…}
MyEJB anEJB = myHome.create();
Background
• Multiple lookups of the same home are
redundant.
• Home is used only once
• an EJBHome never goes stale and can be
reused for the lifetime of the client
application.
• Requires a network call if JNDI server is on
a different machine.
Solution
• Place EJB Home lookup code into a
reusable EJB Home Factory, which can
cache EJB Homes for the lifetime of a client
application.
• Reuse the same EJB home instance
throughout the life time of the client.
• Plain java class.
• Singleton
EJBHomeFactory

- HashMap ejbHomes;
- EJBHomeFactory aHomeSingleton
- InitialContext ctx;

+ static getFactory() : EJBHomeFactory


+ lookUpHome(Class aHomeClass) : <<interface>>
EJBHome EJBHome

uses singleton looks up/caches

EJBClient ParticularHome
uses
Client code
AccountHome anAccountHome =
(AccountHome)EJBHomeFactory.getFactory().
lookUpHome(AccountHome.class);
import javax.ejb.*;
import java.rmi.*;
import java.util.*;
import javax.naming.*;

public class EJBHomeFactory


{
private Map ejbHomes;
private static EJBHomeFactory
aFactorySingleton;

Context ctx;
private EJBHomeFactory() throws NamingException
{
ctx = new InitialContext();
this.ejbHomes =
Collections.synchronizedMap
(new HashMap());
}

public static EJBHomeFactory getFactory()


throws NamingException
{
if ( EJBHomeFactory.aFactorySingleton ==
null )
{
EJBHomeFactory.aFactorySingleton =
new EJBHomeFactory();
}
return EJBHomeFactory.aFactorySingleton;
}
public EJBHome lookUpHome(Class homeClass)
throws NamingException
{
EJBHome anEJBHome = (EJBHome)
this.ejbHomes.get(homeClass);

if(anEJBHome == null)
{
anEJBHome = (EJBHome)
javax.rmi.PortableRemoteObject.narrow
(ctx.lookup(homeClass.getName()),
homeClass);
this.ejbHomes.put
(homeClass, anEJBHome);
}
return anEJBHome;
}
}
Business Delegate
Background
• When using Session and/or Message
Façade, the client is coupled to the EJB
layer, creating dependencies between client
and server.
Problems
• Complicates client logic with complex
error handling.
– NamingExceptions, RemoteExceptions,
EJBException to be caught
• Couples the clients directly to EJB and JMS
API’s.
• Lookup and exception handling code
Cont…
• Dependency of client and server
programmers on availability of the
complete and compiled session bean layer.
• Client programmers depend on the
implementation of the Session Façade in
order to compile and test their code.
Cont…
• Places recovery responsibility on clients.
– When a transaction fails.
– May not be necessary to propagate this error to
the end application user and ask them to retry.
– Instead, client code may automatically re-
execute the transaction.
– Client code needs to be explicitly aware of
catching these transactions and re-trying them
Solution
• Client-side Intermediary between a client
and the Session Façade
Solution
• Introduce intermediate class business delegate to
decouple business components from the code that
uses them.
• Manages the complexity of distributed
component lookup, exception handling and
recovery
• Adapts the business component interface to a
simpler interface for use by views.
• Plain java classes
How?
• Clients locally invoke methods on the BD
• BD delegates directly to a method with the
same signature on the session façade, or
populate a JMS message and send it to the
Message Façade.
• Maps one-to-one to session beans
• Wrap multiple message driven beans.
Functions of BD
• Delegate method calls to an EJB.
• Hide EJB specific system exceptions.
– RemoteException and EJBException
– JMS exceptions are caught in the business delegate and
re-thrown to the client as a non-ejb specific exceptions,
such as a BusinessDelegateException.
– Application level exceptions are passed to the client.
• Cache data locally.
• Transparently re-try failed transactions.
• Prototype business delegates can be written that
simply return dummy data
public class ForumServicesDelegate {
ForumServices sb;
public ForumServicesDelegate() throws
DelegateException {
try{
PortalControllerHome home =
(PortalControllerHome)
EJBHomeFactory.getFactory().lookUpHome
(PortalControllerHome.class,
"ForumServices");
this.sb = home.create();
} catch (Exception e){
throw new DelegateException();
}
}
public long addForum (long categoryPK, String
forumTitle, String summary, String imageName)
throws NoSuchCategoryException,
DelegateException {

try{
return sb.addForum(categoryPK, forumTitle,
summary, imageName);
} catch(CreateException e){
throw new DelegateException();
} catch(RemoteException e){
throw new DelegateException();
}
}
}
EJB Command
Problems with SF/BD
• Slower development process.
– Changing a use-case requires editing 3
different files (interface, bean class,
deployment descriptor) plus redeployment
– BD to be changed.
• Server resources often controlled by just
one team in a large corporation. Difficult to
effect any changes on existing classes.
EJB Command
• Developing with a session façade and
business delegates can result in long
change-deploy-test roundtrips, which can
become a bottleneck in a large project.
• The crux of the problem is that the business
logic is being placed in a layer of session
EJB’s, which can be pretty heavy weight to
develop with.
The Command Pattern
• Wrap business logic in lightweight
command objects
• Decouple the client from EJB
• Execute in one network call
• Act as a façade to the EJB layer
• plain java class with gets, sets and an
execute method
How?
• Clients interact with the command object
locally
• Execute within a remote EJB server,
transparently to the client.
• Encapsulate individual units of work in an
application.
• A use case’s business logic encapsulated in
a command.
Command client view

TransferFunds

w ithdraw AccountID
depositAccountID
transferAmount
w ithdraw AccountBalance
depositAccountBalance

setWithdraw AccountID(int)
setDepositAccountID(int)
setTransferAmountID(double)

execute()

getWithdraw AccountBalance()
getDepositAccountBalance()
Client interaction with a
command
• Client creates a Command
• Client sets attributes onto the command
• Client calls the command’s execute method
• Client calls gets on the command until it has
retrieved all the data resulting from the
execution of the command/use case.
Working
• The command is actually transferred to a
remote EJB server and executed within the
EJB servers JVM.
• When the command has completed
executing, it is returned to the client, who
can then call get methods to retrieve data.
Interaction
• The client instantiates the command object
• The client calls an executeCommand method on
the routing logic/CommandExecutor
• The CommandExecutor delegates the call to an
EJBCommandTarget (part of routing logic)
• The command target knows how to send the
command to the command server
• CommandServer is a stateless session bean.
• CommandServer calls the execute method on the
command, which then goes about its business
logic.
Benefits
• Rapid Application Development (RAD)
• Separation of business logic from presentation
logic.
• Forces execution of use cases in single roundtrip.

• Decouples client from EJB.


• Commands can execute locally or produce
dummy data.
• Allows multiple return values.
Disadvantages
• Very coarse-grained transaction control.
Commands can only run under the transaction
settings of the CommandServer that executes
them. The workaround for this is to deploy
multiple command server session beans with
different jndi names and transaction settings
(configured in the deployment descriptors).
• Can’t use security: Since business logic is
embedded in the command beans
• Commands can become unmanageable on large
projects.
Compare Session beans
• Cheaper session beans.
• They are more lightweight,
• Quicker initial development process at the
expense of possibly less maintainability
over time.
Data Transfer Object
or
Value Object
Exchange of data
• Read data from the server for display
purposes
• Change some data on the server by creating,
updating or removing data.
Need
• Transfer bulk data with the server.
– Many parameters in a method call.
– Client gets multiple attributes from a session or
entity bean by executing multiple fine-grained
calls to the server
The wrong way to get data from the server

Client EJB

Network

getAttribute1()

getAttribute2()

getAttribute3()

getAttribute4()

getAttribute5()
Problem
• Each call to the server is a network call
• Blocking on the client while the EJB
server intercepts the call to the server and
performs transaction and security checks
and retrieval
• Each method call executes in its own
separate transaction if the client is not using
Java Transaction API client demarcated
transactions.
To Achieve
• Client exchange bulk data with the server
without making multiple fine-grained
network calls
Solution
• Create plain java classes called Value
Objects or DTOs, which contain and
encapsulate bulk data in one network
transportable bundle.
• A value object is a plain serializable Java
class that represents a snapshot of some
server side data
import java.io.Serializable;
public class SomeValueObject implements
Serializable {
private long attribute1;
private String attribute2;
private String attribute3;
public long getAttribute1();
public String getAttribute2();
public String getAttribute3();
}
The right way to read data from the server

Client SomeValueObject EJB

getSomeValueObject()

getAttribute1()

getAttribute2()

getAttribute3() Network

getAttribute4()

getAttribute5()
How to design the DTO?

• At the start value objects are copies of server side


entity beans (or domain objects) called Domain
Value Objects
• As the needs of the clients are finalized, Domain
Value Objects often become cumbersome as units
of exchange
• Custom Value Objects: value objects that wrap
arbitrary sets of data, completely driven on the
particular needs of the client.
DTO Factory
Need
• When value objects change very often.

• Value object creation and consumption


logic should be implemented so as to
minimize the impact of frequent changes
in the value objects on the rest of the
system.
Problem
• Value Objects have a tendency to change often.
• Domain Value Objects change whenever the
domain objects change (adding a new attribute to
an entity bean, etc).
• Custom Value Objects are just use case specific
data holders for transporting data across a
network; they can change as frequently as your
application’s presentation view.
• Tens to hundreds of different Value Objects, each
of which would require custom logic to create it.
• How and where should this logic be implemented?
Solution 1
• getXXXValueObject / setXXXValueObject
methods directly on entity beans.
• Tightly couples the value object layer to the
entity bean layer.
• Every time a web page changed and a different
view of the data model was required, you would
have to add a new method to an entity bean,
recompile your entity bean, and redistribute your
remote interfaces to any client using them.
• Entity beans are are no longer reusable.
Solution
• Place the responsibility for creating and
consuming Value Objects in a
ValueObjectFactory or a DTO Factory.
Benefits
• Separates the logic related to Value Objects (part
of the application domain) from other
components in your system such as entity beans
(part of the business domain).
• When new views or different subsets of server
side data become necessary, new value object
creation methods can be added to the
ValueObjectFactory.
• The entity beans themselves do not need to know
about these different views of their data.
Benefits
• Better maintainability. Entity beans no
longer need to be changed and recompiled
when the needs of the client change.
• Enables entity bean reuse.
Business Interface
Need
• The enterprise bean class MUST provide an
implementation of all methods declared in
the Remote or Local interface
Need
• The EJB developer must manually maintain
consistency between interface definition and
bean implementation.
• No automatic way to detect these problems at
compile time.
• server vendor’s proprietary post-compilation tool.
• How can inconsistencies between remote/local
interface methods and the enterprise bean
implementation be discovered at compile-time?
Solution 1
• The enterprise bean directly implement the
remote or local interface in the bean class.
• This would enforce consistency between method
definition and implementation, using any standard
java compiler.
• EJB specification advises against this practice
• These interfaces define extra methods
(isIdentical, getPrimaryKey, remove, etc), which
are meant to be implemented by the EJBObject
and EJBLocalObject stubs, not the enterprise bean
class.
EJBObject and EJBLocalObject Interfaces
<<interface>>
java.rmi.Remote

<<interface>> <<interface>>
javax.ejb.EJBObject javax.ejb.EJBLocalObject

getEJBHome() getEJBLocalHome()
getHandle() getPrimaryKey()
getPrimaryKey() isIdentical(obj)
isIdentical(obj) remove()
remove()

<<interface>> <<interface>>
Remote Local

businessMedthod1() businessMedthod1()
businessMedthod2() businessMedthod2()
.... ....
Cont…
• Clutters your enterprise bean class by writing
dummy implementations of these extra methods.
• The bean could be directly cast to one of these
interfaces, allowing a developer to pass an
instance of this to a client.
• Not allowed by the EJB specification.
• To pass a reference to one-self, a bean needs
should first get a reference to itself by calling
getEJBObject or getEJBLocalObject off of the
SessionContext or EntityContext interface.
Solution
• Create a super interface called a Business
Interface, which defines all business
methods.
• Both the remote/local interface and the
enterprise bean class implement this
interface, forcing compile-time consistency
checks.
Business Interface for Remote and Local Beans

<<interface>>
java.rmi.Remote

<<interface>> <<interface>> <<interface>> <<interface>>


javax.ejb.EJBObject BusinessRemote javax.ejb.EJBLocalObject BusinessLocal

getEJBHome() businessMedthod1() getEJBLocalHome() businessMedthod1()


getHandle() throws RemoteException() getPrimaryKey() businessMedthod2()
getPrimaryKey() businessMedthod2() isIdentical(obj) ....
isIdentical(obj) throws RemoteException remove()
remove() ....

<<interface>> EnterpriseBean <<interface>> EnterpriseBean


Remote Local
attribute1 attribute1
attriute2 attriute2
... ...

businessMedthod1() businessMedthod1()
businessMedthod2() businessMedthod2()
.... ....
//EJB Methods //EJB Methods
.... ....
JDBC for Reading
JDBC for Reading
• Present static server-side data to a client in
tabular form.
• Read-only
HTML Table of Employees

Employee Department

Adam Berman Development


Eileen Sauer Training
Ed Roman Management
Clay Roach Architecture
Solution 1
• Employee and a Department entity bean.
• getEmployees() method on a Session Façade
• finder method on an EmployeeHome object
• find each employee’s related department entity
bean
• Create a Custom Data Transfer Object with the
combined data from these two entity beans.
• Session bean returns a collection of
EmployeeDepartmentDTOs.
Problems
• The n+1 entity bean database calls problem.
• With BMP and certain implementations of CMP,
retrieving data from N entity beans require N+1
database calls.
– finder method (one database call).
– ejbLoad() individually on each entity bean returned by
the finder method, lock a db conn of the pool, n/w call,
• Employee and Departments example: 2N+1
database calls
Cont…
• Remote Call overhead. If going through the entity
bean remote interface (as opposed to the local
interface), this method would also require 3N
remote calls for N rows of employee and
department data. The remote calls break down
as follows:
– N calls to getValueObject() for each Employee.
– N calls to getDepartment() on each Employee.
– N calls to getValueObject() on each Department.
• Require tens of lines of code
• Significantly slow down a system due to
– the database calls
– remote calls
– overhead incurred for traversing multiple entity
bean relationships
Need
• Client side mainly requires tabular data for read
only, listing purposes
• Using local interfaces will reduce the performance
problems
• Querying through the entity bean layer for simply
listing of read only data causes unacceptable
performance problems
• In BMP, perform listing operations on relational
databases using JDBC.
• Using JDBC to directly read the rows and columns
required by the client can be far faster and more
efficient then going through the entity bean layer.
• Entire table of employees and departments could
be bulk-read in just one JDBC call from the
database
• After reading in the ResultSet return using,
– EmployeeDepartmentDTO
– HashMaps
– RowSets
Benefits
• Perform queries in ONE BULK READ.
• Takes advantage of DB built in caching.
• The next time a query is run, the one bulk JDBC
query will come directly from the database cache.
• Retrieve the exact data your use case requires.
Using JDBC, you can select the exact columns
required across any number of tables.
Tradeoffs
• Tight coupling between business and
persistence logic. Data Access Object can
be used to alleviate this problem.
• Bug prone and less maintainable.
• Changes to the database schema will require
changes to multiple code fragments across
the Session Façade.Data Access Object
pattern can help here.
Data Transfer HashMap
Data Transfer HashMap
• A client needs to exchange bulk data with
the server in a generic fashion.
Problems with DTO
• Minimizing on the number of network calls
• Data Transfer Objects help in this.
• new DTO for every new use case
• High cost to change: new DTOs and
associated creation logic must be written.
Cont…
• Data Transfer Objects create a new layer, which
can explode to thousands of objects in a large
application.
• Changes in entity bean attributes will cause ripples
that could require changes in multiple DTOs as
well.
• Client UI’s tightly coupled to the server.
• Each client UI is tightly coupled to the DTO it
uses
Solution
• Use HashMaps to marshal arbitrary sets of
data between client and EJB tier.
• Plain JDK HashMaps provide a generic,
serializable container for arbitrary sets of data, that
can replace an entire layer of Data Transfer
Objects.
• The only dependency been client and server side
code is the naming conventions placed on the
keys used to identify attributes, described later in
this pattern.
• Clients request HashMaps by going through the
Session Façade.
Using a Data Transfer HashMap

Client AccountHashMap SessionFacade

getAccountData()

get("accountNumber")

get("name")

get("passw ord") Netw ork

get("balance")

put("balance", 123)

setAccountData(aHashMap)
• A client is passed a HashMap that contains
different sets of data, as needed by the particular
use case.
• Using a HashMap instead of a data transfer object
comes at the cost of additional implementation
complexity, since the client now needs to
explicitly know the strings used as keys to query
the HashMap for the attributes of interest.
• The session façade and the client agree on the
strings to be used by to populate and read from a
HashMap.
Benefits
• Excellent maintainability - eliminates the
Data Transfer Object layer.
• One data object (Map) across all clients. A Map
of attributes is reusable from the Session Façade
down to the JSPs. In particular, using a Map as a
data container significantly reduces the
complexity of the JSP code, as pages don’t need
to be written with use case specific Value Objects
that are tightly coupled to the entity bean layer.
• Low cost of maintenance over time. New views
of server side data can be created that does not
require any server side programming. Clients can
dynamically decide which attributes to display.
Tradeoffs
• Need to maintain a contract for attribute
keys. Extra dependency between client and
server.
• Loss of strong typing/compile time
checking.
Data Transfer Rowset
Data Transfer RowSet
• When using JDBC for Reading, relational
data needs to be transferred across the
network tier from the Session Façade to the
client.
• How can relational data be transferred to
the client in a generic, tabular format?
public class EmployeeDepartmentViewObject
{
public String employeeName;
public String employeeTitle;
...
public String departmentName;
public String departmentLocation;
...
}
• Session bean performs a JDBC call to get a
ResultSet that contains information about an
employee and their department.
• Session bean manually extracts fields from the
ResultSet and calls the necessary setters to
populate the DTO.
• Each row in the ResultSet will be transferred into
a DTO which will be added to a collection.
• This collection of DTO’s now forms a network
transportable bundle, transferable to the client for
consumption.
Problem
• Tabular to OO and back to tabular is
redundant.
• Preserve the tabular nature of the data being
transferred in a generic fashion, allowing
for simpler clients and simpler parsing into
the client UI.
Solution
• Use RowSets for marshalling raw relational
data directly from a ResultSet in the EJB
tier to the client tier.
RowSet
• An interface, a subclass of java.sql.ResultSet.
• CachedRowSet implementation allows you to
wrap ResultSet data and marshal it off to the client
tier.
• Client can operate directly on the rows and fields.
• CachedRowSet is disconnected from the database.
• Once the CachedRowSet has been initialized with
data, database connections can be closed – the
CachedRowSet now maintains a copy of the
results of the SQL query.
Value Objects vs. RowSets

{
Employee Department

Adam Berman Development


Client side Eileen Sauer Training
Table UI
Ed Roman Management
Clay Roach Architecture

{ }
Adam Berman Development
Adam Berman | Development Eileen Sauer Training
Ed Roman Management
Collection of Clay Roach Architecture
Employee Eileen Sauer | Training Single
Department RowSet
OR

Custom Data Object


Transfer Ed Roman | Management

Objects
Clay Roach | Architecture
Advantages
• Provides a common interface for all query
operations.
• All the clients can use the same interface for
all data querying needs.
• No matter what the use case is or what data
is being returned, the interface a client
operates on stays the same.
• Eliminates the redundant data translation.
Tradeoffs
• Clients need to know the name of database table
columns. Maintaining a ‘contract’ of attribute
names between client and server, as described in
the Generic Attribute Access pattern.
• Not object oriented.
• No compile-time checking of query results. Rather
than calling getXXX() on a value object, a client
must now call getString(“XXX”) on the RowSet,
possibility of mistyping the attribute name.
Data Access Object
Problem
• Access to data varies depending on the source of
the data. Access to persistent storage, such as to a
database, varies greatly depending on the type of
storage (relational databases, object-oriented
databases, flat files, and so forth) and the vendor
implementation.
• Code that depends on specific features of data
resources ties together business logic with data
access logic. This makes it difficult to replace or
modify an application's data resources.
Participants
• BusinessObject : requires access to the data source to
obtain and store data. a session bean, entity bean, Java
object, a servlet or helper bean.
• DataAccessObject : abstracts the underlying data access
implementation. Enables transparent access to the data
source.
• DataSource : a database such as an RDBMS, OODBMS,
XML repository, flat file system, another system
(legacy/mainframe), service (B2B service or credit card
bureau), or some kind of repository (LDAP).
• TransferObject : This represents a Transfer Object used
as a data carrier. To return/receive the data to/from the
client.
Benefits
• Separates a data resource's client interface
from its data access mechanisms
• Adapts a specific data resource's access API
to a generic client interface
• Allows data access mechanisms to change
independently of the code that uses the data.
References
• EJB Design Patterns by Floyd Marinescu - Wiley.
(available online at www.theserverside.com and
\\Kalpa)
• J2EE Design Patterns Catalog -
http://java.sun.com/blueprints/patterns/cata
log.html
• Core J2EE Patterns (online book) -
http://java.sun.com/blueprints/corej2eepatte
rns/Patterns/
• Sun Java Center J2EE Patterns -
http://developer.java.sun.com/developer/rest
ricted/patterns/J2EEPatternsAtAGlance.html

S-ar putea să vă placă și