I. Prérequis▲
Vous pouvez utiliser l'IDE que vous souhaitez pour suivre ce tutoriel. Le code source des exemples présentés dans ce tutoriel est disponible dans ce repository Git, et plus particulièrement dans cette classe.
Les outils dont vous aurez besoin pour suivre ce tutoriel sont :
- JDK 8 d'Oracle ;
- Maven 3.
N'hésitez pas à lire le tutoriel Maven 3 si vous souhaitez en apprendre plus sur ce dernier.
II. Qu'est-ce que l'AOP ?▲
L'AOP (« Aspect Oriented Programming » ou « Programmation Orientée Aspect » en français) n'est pas à proprement parler un langage de programmation pour créer des programmes. C'est un concept qui permet d'exécuter une action lorsqu'un événement est déclenché (ex. : une méthode qui est exécutée). On peut donc voir l'AOP comme une extension au langage de programmation utilisé dans le projet.
III. Dans quel contexte utilise-t-on l'AOP ?▲
Lorsque l'on programme par aspect, on distingue le code métier du code technique. L'AOP permet de factoriser le code technique du projet. Le terme crosscutting concerns (ou « fonctionnalités transversales » en français) est utilisé pour désigner cette partie technique.
Pour citer quelques exemples de crosscutting concerns, l'AOP peut être utilisé pour :
- ouvrir/fermer des ressources (ex. : Démarrer et commiter/rollbacker une transaction) ;
- vérifier les permissions de l'utilisateur (sécurité) ;
- du cache ;
- des loggers ;
- du monitoring ;
- des benchmarks ;
- interrompre une méthode qui prend beaucoup de temps pour s'exécuter ;
- dans une moindre mesure : changer le comportement d'une méthode pour retourner un nouveau résultat.
Voici un exemple de code sans l'AOP :
public
void
addPermissionsToUser
(
Permission[] permissions, User user)
throws
SQLException {
try
{
// Code technique : SI aucune transaction n'est en cours ALORS
// Code technique : créer la transaction (cf: START TRANSACTION en SQL)
// Code technique : FIN SI
// Code métier : Ajouter des droits à l'utilisateur
}
catch
(
SQLException e) {
// Code technique : SI une exception est lancée ALORS
// Code technique : rollbacker
// Code technique : propager l'exception
// Code technique : FIN SI
}
finally
{
// Code technique : SI une exception n'est pas lancée ET la méthode courante est la dernière méthode transactionnelle exécutée ALORS
// Code technique : commiter
// Code technique : FIN SI
}
}
Voici un exemple de code avec l'AOP :
@Transactional
public
void
addPermissionsToUser
(
Permission[] permissions, User user) {
// Code métier : Ajouter des droits à l'utilisateur
}
IV. Quelques dates sur l'AOP▲
- Juillet 1996 : la première apparition de l'AOP est attribuée à MTS (Microsoft Transaction Service) avec Windows NT 4.0, pour avoir mis en place un système d'interception pour les transactions et la sécurité.
-
L'AOP en Java avec AspectJ :
- 1997 : le projet AspectJ est créé par Gregor Kiczales et ses collègues, alors employés de la société Xerox PARC,
- 2001 : une première version publique est publiée,
- décembre 2002 : le projet est confié à la fondation Eclipse,
- 20 décembre 2005 : l'introduction des annotations dans Java 5 (4 octobre 2004) permet au projet AspectJ 5 d'avoir une nouvelle syntaxe à base d'annotations. Il est alors possible d'inclure du code AspectJ directement dans du code Java ;
- Le framework Spring intègre le concept d'AOP avec AspectJ, mais ce dernier limite les possibilités d'AspectJ à l'exécution des méthodes publiques.
V. AspectJ avec Spring▲
Il existe deux moyens pour mettre en place l'AOP avec AspectJ :
- En incluant directement les concepts AspectJ dans du code Java, votre code Java sera alors présent dans un fichier d'extension « .aj » et vous aurez besoin d'un plugin qui permettra à votre IDE de comprendre la syntaxe.
- En utilisant des annotations Java.
Dans ce tutoriel, nous verrons la deuxième solution.
V-A. Dépendances Maven▲
Ajoutez la dépendance ci-dessous dans votre pom.xml :
<dependency>
<groupId>
org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-aop</artifactId>
</dependency>
Utilisez l'annotation @EnableAspectJAutoProxy
pour activer l'AOP. Vous pouvez placer cette annotation sur votre classe main, ou bien créer une classe de configuration comme ci-dessous :
// Cette classe de configuration permet d'activer les aspects.
// Vous pouvez la laisser vide.
@Configuration
@EnableAspectJAutoProxy
public
class
AspectConfig {
}
V-B. Les annotations AspectJ▲
|
Crée un alias pour une portion de condition(s). |
|
S'exécute AVANT la méthode appelée. La méthode peut prendre en paramètre un JoinPoint. |
|
Similaire à, |
|
S'exécute lorsqu'une méthode est appelée, permet d'exécuter du code avant et après l'exécution de la méthode appelée (comme les filtres web). La méthode peut prendre en paramètre un ProceedingJoinPoint. |
V-C. Syntaxe AspectJ▲
Il faut placer les exemples ci-dessous dans les annotations (au choix) : @Pointcut
, @Before
, @Around
, ou @After
.
Template |
expression(<method scope> <return type> <fully qualified class name>.*(parameters)) |
Syntaxe représentant toutes les méthodes |
execution(* *(..)) |
Syntaxe représentant toutes les méthodes ayant une annotation |
@annotation(mypackage.MyAnnotation) && execution(* *(..)) |
V-D. Exemple avec les loggers▲
Créons une classe aspect « RestLogAspect » qui va afficher une ligne de log à chaque fois qu'une route, c'est-à-dire une méthode de notre annotée avec @RequestMapping
, sera appelée.
Notre ligne de log aura le format suivant :
[REST][<durée-méthode-exécutée>ms][<nom-rest-contrôleur>][<nom-méthode-http>][<chemin-http>][<nom-méthode-contrôleur>()] <valeurs-des-paramètres>
Ainsi, l'exécution de la méthode sayHello
(
) du contrôleur ci-dessous :
@RestController
public
class
HelloWorldController {
@RequestMapping
(
path =
"/hello/{name}"
, method =
RequestMethod.GET)
public
ResponseEntity<
String>
sayHello
(
@PathVariable
(
"name"
) String name) {
return
new
ResponseEntity<>(
"hello "
+
name, HttpStatus.OK);
}
}
… affichera ceci :
[REST][31ms][HelloWorldController][GET][/hello/{name}][sayHello()] world
Implémentation de notre classe (source) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
/**
* An aspect class for logging all routes
*
*
@author
Gokan EKINCI
*/
@Component
@Aspect
@Log
public
class
RestLogAspect {
@Pointcut
(
"@annotation(org.springframework.web.bind.annotation.RequestMapping)"
)
public
void
restAnnotation
(
) {}
@Around
(
"restAnnotation() && execution(* *(..))"
)
public
Object restLog
(
ProceedingJoinPoint joinPoint) throws
Throwable {
final
MethodSignature signature =
(
MethodSignature) joinPoint.getSignature
(
);
final
Method method =
signature.getMethod
(
);
final
Class<
?>
controllerClass =
joinPoint.getTarget
(
).getClass
(
);
final
RequestMapping controllerAnnotation =
controllerClass.getAnnotation
(
RequestMapping.class
);
final
String controllerRestPath =
(
controllerAnnotation.path
(
).length ==
0
) ? ""
: controllerAnnotation.path
(
)[0
];
final
RequestMapping controllerMethodAnnotation =
method.getAnnotation
(
RequestMapping.class
);
final
String methodRestPath =
(
controllerMethodAnnotation.path
(
).length ==
0
) ? ""
: controllerMethodAnnotation.path
(
)[0
];
final
RequestMethod requestMethod =
(
controllerMethodAnnotation.method
(
).length ==
0
) ? RequestMethod.GET : controllerMethodAnnotation.method
(
)[0
];
final
Instant start =
Instant.now
(
);
// @Before code
Object returnValue =
null
;
Throwable throwable =
null
;
try
{
returnValue =
joinPoint.proceed
(
);
}
catch
(
Throwable throwable1) {
throwable =
throwable1;
log.warning
(
String.format
(
"[RestLogAspect] : %s"
, throwable1.getMessage
(
)));
}
// @After code
final
String logMessage =
String.format
(
"[REST][%sms][%s][%s][%s][%s()] %s"
,
Duration.between
(
start, Instant.now
(
)).toMillis
(
),
controllerClass.getSimpleName
(
),
requestMethod,
controllerRestPath +
methodRestPath,
method.getName
(
),
Arrays.stream
(
joinPoint.getArgs
(
))
.map
(
argument ->
(
argument !=
null
) ? argument.toString
(
) : "null"
)
.collect
(
Collectors.joining
(
", "
))
);
log.info
(
logMessage);
if
(
throwable !=
null
) {
throw
throwable;
}
return
returnValue;
}}
Ce qu'il faut retenir de cette implémentation :
- Nous avons une classe annotée avec
@Component
et@Aspect
pour indiquer à Spring que nous avons un bean de type aspect. L'annotation@Log
est un simple utilitaire de la bibliothèque Lombok pour éviter de créer explicitement un attribut log de type Logger, n'en tenez pas compte. - Nous avons créé une méthode annotée avec
@PointCut
pour créer un alias « restAnnotation() », que nous utiliserons dans l'annotation@Around
de la méthode aspect «restLog
(
) ». Cette étape est facultative, mais augmente la lisibilité de l'expression AspectJ utilisée dans l'annotation@Around
. - Nous avons créé une méthode aspect
restLog
(
) annotée avec@Around
. Celle-ci va se déclencher lorsque l'expression indiquée dans l'annotation va matcher avec la méthode exécutée. Dans notre cas, lorsqu'une méthode annotée avec@RequestMapping
va s'exécuter, notre méthode aspectrestLog
(
)va s'exécuter aussi. - La méthode
restLog
(
) possède en paramètre le type ProceedingJoinPoint pour récupérer toutes les informations nécessaires par rapport à la méthode exécutée.
VI. Remerciements▲
Je tiens à remercier ClaudeLeloup et -FloT- pour la relecture orthographique.