image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Les beans sont des composants interconnectables utilisés au sein des couches d'accès aux données et métier d'une application. Ils permettent notamment d'initialiser facilement les composants d'une couche ainsi que d'assurer la communication entre composants de différentes couches. Nous réalisons ici une introduction à leur utilisation avec l'API EJB 3.1 de Java Enterprise Edition 6.

On pourra se référer à ce tutoriel d'Oracle pour plus d'informations sur l'utilisation des beans.

Que sont les beans ?

Comment exécuter des beans ?

Utilisation d'une implantation lite

public class BeanTester 
{
	public static void main(String[] args) 
	{
		EJBContainer container = EJBContainer.createEJBContainer();
		Context context = container.getContext();
		TestBean testBean;
		try (EJBContainer container = EJBContainer.createEJBContainer();)
		{
			Context context = container.getContext();
			TestBean testBean = (TestBean)context.lookup("java:app/bin/TestBean");
			testBean.doSomeAction();
		} catch (NamingException ex) 
		{
			System.err.println("Problem while fetching the bean");
		}
	}
}

Utilisation d'un serveur applicatif (exemple de WildFly)

Nous pouvons utiliser par exemple Wildfly qui sera utilisé comme serveur applicatif fournissant l'implantation complète de l'API Java EE.

L'installation et la configuration basique de WildFly est assez simple :

  1. On télécharge l'archive contenant les fichiers binaires de la dernière version stable de WildFly sur le site wildfly.org (9.0.1 à l'écriture de ce
  2. On extrait l'archive dans un répertoire
  3. Le serveur WildFly peut être lancé par le script wildflyDirectory/standalone.sh ; on pourra rajouter l'option -Djboss.socket.binding.port-offset=N si l'on souhaite choisir le numéro de port d'attache du serveur web (par défaut 8080 auquel on peut rajouter l'offset défini N)
  4. Il faut ensuite se rendre avec un navigateur web sur http://localhost:8080 (8080 à remplacer par le port réel du serveur) et suivre les instructions qui demanderont de créer au moins un utilisateur avec le script addUser.sh pour pouvoir par la suite accéder à la console d'administration
  5. On pourra demander l'installation de greffons supportant JBoss/Wildfly depuis son IDE favori ; par exemple sous Eclipse, depuis le menu Help, on accède au marketplace et on installe la dernière version des JBoss Tools

Les beans entité

Les beans session

Bean session Stateful

Bean session Stateless

Bean session Singleton

Voici un exemple d'un bean Singleton ABean initialisé au démarrage et qui entraîne l'instantiation prélable de CBean puis de BBean :

@Singleton @Startup @DependsOn("BBean") public class ABean { ... }
@Singleton @DependsOn("CBean") poublic class BBean { ... }
@Singleton public class CBean { ... }

Cycle de vie d'un bean session

Lorsqu'une nouvelle instance d'un bean session est nécessaire, celle-ci est créé par le container, cela requiert :

  1. d'appeler le constructeur par défaut sans argument
  2. d'injecter les dépendances du bean (chacune des dépendances est indiquée en annotant le champ par @EJB
  3. d'appeler les éventuelles méthodes de post-construction : des méthodes sont annotées par @PostConstruct (il s'agit d'une méthode sans argument)

Les beans session de type Stateful peuvent être mis en hibernation : il s'agit de les transférer de la mémoire centrale vers un espace de stockage secondaire. Avant cette mis en hibernation les méthodes annotées @PrePassivate sont appelées ; lorsque l'hibernation se termine et le bean replacé en mémoire centrale, les méthodes annotées @PostActivate sont appelées.

Un bean Stateful peut également être détruit par son client. Celui-ci appelle la méthode annotée @Remote pour ce travail. Suite à l'appel de cette méthode, le bean est supprimé.

Avant de détruire définitivement un bean, le container appelle ses méthodes (sans argument et de type de retour void) annotées @PreDestroy : on peut les utiliser pour libérer d'éventuelles ressources précédemment acquises.

L'injection de dépendances

Des annotations peuvent être placées sur les champs nécessitant une injection de dépendances :

Utilisation d'@Inject avec annotations

Prenons pour exemple l'injection d'une instance de B dans une instance de A :

public class A { @Inject private B b; }
public interface B { ... }
public class BDefault implements B { ... }

Dans le cas présent, le container recherche pour injecter dans le champ b de A toutes les classes compatibles : il s'agit de B et BDefault. B étant une interface, elle ne peut être instantiée : seule BDefault peut être instantiée : le container l'instantie et l'injecte dans le champ b.

Si plusieurs possibilité d'injection existent, il est nécessaire d'utiliser une annotation supplémentaire pour lever l'amibiguïté ; cela peut être une annotation personnalisée ou alors l'annotation @Named.

Nous pouvons par exemple avoir deux implantations de B avec deux annotations @Named différentes :
@Named("first") public class BDefault1 implements B { ... }
@Named("second") public class BDefault2 implements B { ... }

On peut alors lever l'ambiguïté sur la classe à utiliser pour le champ b de A en ajoutant une annotation @Named
public class A { @Inject @Named("second") private B b; }

Ici, on utilisera BDefault2 qui correspond à l'annotation indiquée.

Quelquefois, il est souhaitable de sélectionner dynamiquement la dépendance à utiliser. Dans ce cas de configure, on utilise Instance :

public class A { @Inject private Instance<B> b; }

A l'exécution, on pourra interroger Instance avec un itérateur pour récupérer la dépendance qui nous convient le mieux :
for (B impl: this.b)
	System.out.println("Here is an implementation of B: " + b);

Il est possible de filtrer une Instance pour obtenir une Instance plus précise avec la méthode select en précisant des annotations :
Annotation a = Named("second");
Instance<B> refinedInstance = this.b.select(a);

La méthode get() de Instance fonctionne pour récupérer l'unique dépendance satisfaisant les conditions. S'il en reste plusieurs une exception AmbiguousResolutionException est levée.

Il est également possible d'annoter des méthodes ou constructeurs avec l'annotation @Inject. Dans ce cas, le constructeur ou la méthoed est appelée automatiquement à la création de la classe et le résolveur d'injection essaie de trouver une instance valable pour chaque paramètre.

Pattern Observer

Le mécanisme d'injection peut être utilisé pour faciliter la mise en place d'un pattern Observer.

Prenons pour exemple une application web générant un événement à chaque page consultée. On écrit la classe PageAccess :

public class PageAccess
{
	public InetAddress visitorAddress;
	public int statusCode; // 200 for success, 404 for page not found...
	public String visitedPage;
	
	public PageAccess(InetAddress visitorAddress, int statusCode, String page)
	{
		this.visitorAddress = visitorAddress;
		this.statusCode = statusCode;
		this.visitedPage = visitedPage;
	}
	
	@Override
	public String toString()
	{
		return String.format("[%s] %s: %d", visitorAddress, visitedPage, statusCode);
	}
}

On peut écrire une classe écoutant les événements de type PageAccess et les enregistrant dans un fichier journal :

@Stateless
public class PageAccessLogger
{
	private String journalLocation;
	private String encoding;
	
	private Writer journalWriter;
	
	@PostConstruct
	public void init()
	{
		journalWriter = 
			new OutputStreamWriter(FileOutputStream(journalLocation, true), encoding)); // we append data into the journal
	}
	
	public void log(PageAccess pa)
	{
		journalWriter.write(pa.toString());
	}
	
	public void listenToPageAccess(@Observes PageAccess pa)
	{
		log(pa);
	}
}

PageAccessLogger est automatiquement instantié et sa méthode listenToPageAccess est appelée par le container lorsqu'un événement de type PageAccess est émis. Il nous reste maintenant à écrire l'observé émettant les événements (cela peut être un servlet) :

@WebServlet(value="/HelloWorld")
public class HelloWorldServlet
{
	@Inject Event<PageAccess> pageAccessEvent;
	
	public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		pageAccessEvent.fire(new PageAccess(request.getRemoteAddress(), request.getServletPath(), response.getStatus());
	}
}

Il sera toujours possible d'écrire de nouveaux producteurs de l'événement PageAccess (observés) ainsi que de nouveaux consommateurs de l'événements (observateurs), le container se chargeant de les mettre en relation.

Les portées des objets

Chaque objet géré par le container a une portée spécifique ; on dénombre les portées suivantes (qui sont indiquées par annotations) :

Il est possible de forcer l'instanciation d'un nouveau bean (sans prendre en compte sa portée de définition) en utilisant l'annotation @New sur le site de l'injection.

Descripteur de déploiement

Le nommage de bean

Afin de pouvoir être facilement récupéré, un bean peut être associé à un nom (chaîne de caractères ou objet implantant l'interface Name). On utilise l'API Java Naming and Directory Interface (JNDI) à cet effet. Pour réaliser cette tâche JNDI peut s'aider de greffons se connectant à des services de nommage (service provider) utilisant différents protocoles (LDAP, NIS, DNS, CORBA, système de fichier...). En pratique, pour le nommage des beans, les espaces de nommage suivants peuvent être utilisés pour un nommage plus ou moins relatif :

Quelques informations sur les noms utilisés :

Il est possible de rajouter au nom un suffixe !interface : celui-ci est uniquement utile si le bean implante plusieurs interfaces différentes (avec des annotations divergentes telles que @local ou @remote). Cela permet d'indiquer quelle interface on souhaite utiliser pour manipuler le bean.

L'accès aux interfaces métiers

@Local public interface Business {}
@Stateless public class BusinessBean implements Business {}

public interface Business {}
@Stateless @Local(Business.class) public class BusinessBean implements Business {}

Message Driven Bean (MDB)

Présentation

Un exemple de MDB

Nous souhaitons écrire un bean recevant des notes à ajouter pour un utilisateur donné. JMS supporte différents types de messages :

La note est un objet sérializable et le propriétaire de la note est une chaîne de caractères que nous pouvons placer dans une MapMessage.

Nous écrivons le MDB chargé de traiter les notes reçues :

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup",
            propertyValue = "jms/NoteQueue"),
    @ActivationConfigProperty(propertyName = "destinationType",
            propertyValue = "javax.jms.Queue")
})
public class NoteReceiverBean implements MessageListener 
{
    @Resource
    private MessageDrivenContext mdc;
    
    @EJB
    private NoteBackend noteBackend; // the note backend is automatically injected by the container
    
    static final Logger logger = Logger.getLogger("NoteReceiverBean");

    public NoteReceiverBean() { }

    @Override
    public void onMessage(Message receivedMessage) 
    {
        try {
            if (received instanceof TextMessage) 
            {
		noteBackend.addNote(receivedMessage.getObject("note"), receivedMessage.getString("owner"));
            } else {
                logger.log(Level.WARNING, "Message of wrong type: {0}", receivedMessage.getClass().getName());
            }
        } catch (JMSException e) {
            logger.log(Level.SEVERE, "SimpleMessageBean.onMessage: JMSException: {0}", e.toString());
            mdc.setRollbackOnly();
        }
    }
}

Ce bean peut être utilisé par un client qui souhaiterait ajouter une nouvelle note pour un utilisateur donné. Les messages doivent être créés par un MessageProducer obtenu avec une ConnectionFactory. Les notes sont envoyées dans une queue de messages dédiée nommée jms/NoteQueue.

@Resource(lookup = "java:comp/DefaultJMSConnectionFactory")
private static ConnectionFactory connectionFactory;
    
@Resource(lookup = "jms/NoteQueue")
private static Queue queue;

Voici maintenant comment envoyer un message :

public void sendNote(Note note, String owner)
{
	try (Connection connection = connectionFactory.createConnection();) 
	{
		Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
		MessageProducer messageProducer = session.createProducer(queue);
		Message message = session.createMapMessage();
		message.setObject("note", noteToSend);
		message.setString("owner", noteOwner);
		messageProducer.send(message);
	}
}

Timers

Par exemple, une méthode devant être appelé tous les samedis matin à 4h15 et tous les 1ers jours du mois à 7h00 :

@Schedules ({
	@Schedule(dayOfWeek="Sat", hour="4", minute="15"),
	@Schedule(dayOfMonth="1", hour="7", minute="0") })
public void doSomeAction()
{
}

Il est possible aussi de planifier des tâches programmatiquement en utilisant un TimerService :

...
@Resource
TimerService timerService; // the timerService is automatically injected

public void planAction()
{
	timerService.createSingleActionTimer(10000, new TimerAction()); // we trigger the @timeout annotated method of the TimerAction class in 10 seconds
}

Nous devons aussi définir l'action à déclencher dans le délai indiqué avec une classe disposant d'une méthode avec l'annotation Timeout :

class TimerAction
{
	@Timeout
	public void doSomeAction()
	{
		...
	}
}

Intercepteurs

À titre d'exemple, écrivons un intercepteur loggant les appels de méthodes :

public class LoggingInterceptor
{
	Logger logger = Logger.getLogger(LoggingInterceptor.class.getName());
	
	@AroundInvoke
	public Object interceptAndLog(InvocationContext ctx)
	{
		Object result = ctx.proceed(); // call the method itself or the next interceptor on the chain
		String message = String.format("Method %s called on instance %s with parameters %s, returning result: %s, ctx.getMethod(), ctx.getTarget(), Arrays.toString(ctx.getParameters()), result);
		logger.info(message);
		return result;
	}
}

On peut ensuite l'utiliser pour n'importe quel bean avec une annotation :

@Interceptors({LoggingInterceptor.class, AnotherInterceptor.class})
@Stateless
public class MyBean
{
	...
}

On aurait pu alternativement indiquer l'intercepteur dans le fichier de déploiement ejb-jar.xml :

<interceptor-binding>
	<target-name>myapp.MyBean</target-name>
	<interceptor-class>myapp.LoggingInterceptor.class</interceptor-class>
	<interceptor-class>myapp.AnotherInterceptor.class</interceptor-class>
</interceptor-binding>

Il est également possible de définir des intercepteurs pour des timers avec l'annotation @AroundTimeout.

Transactions

Avec Java EE, les transactions sur des bases de données pour manipuler des données persistantes peuvent être traitées de deux manières :

Mode bean

Si le bean gère lui-même les transactions, on peut insérer une dépendance pour récupérer un gestionnaire de transactions et ensuite appeler ses méthodes begin() pour démarrer la transaction puis commit() ou rollback() pour l'annuler.

@Resource
private UserTransaction transaction;

Mode container

Pour chaque méthode, il faut ajouter éventuellement une annotation @TransactionAttribute(...) pour indiquer son comportement vis-à-vis des transactions :

Le comportement par défaut REQUIRED est généralement acceptable dans la plupart des cas.

Le commit est automatiquement réalisé en fin de transaction. Touefois si une exception système survient un rollback est effectué. On peut également réaliser soi-même une opération rollback dans la méthode du bean comme le montre l'exemple suivant :

@Stateless
public class ExempleBean
{
    @Resource
    private EJBContext context;

    public void someTrxnMethod() throws Exception {
        try {
            // persistence actions
        }
        catch(Throwable t) {
            context.setRollbackOnly(); // trigger the rollback
        }
    }
}