29 – Janino (Compilateur Java Dynamique)
Version : 4.0.0 Date : 2026-01-17
1. Introduction
Janino est un compilateur Java embarqué qui permet de compiler du code source Java en bytecode JVM à la volée. Contrairement au ScriptEngine existant (interprété), Janino offre des performances natives car le code est réellement compilé.
Positionnement
┌─────────────────────────────────────────────────────────────────┐
│ Socle V004 Scripts │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ScriptEngine │ │ JaninoEngine │ │
│ │ (existant) │ │ (NOUVEAU) │ │
│ ├─────────────────────┤ ├─────────────────────┤ │
│ │ - JavaScript │ │ - Java pur │ │
│ │ - BeanShell │ │ - Bytecode natif │ │
│ │ - Interprété │ │ - Haute performance │ │
│ │ - Typage dynamique │ │ - Typage statique │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Cas d’usage
| Situation | Recommandation |
|---|---|
| Calculs financiers (frais, taxes) | Janino |
| Validations métier complexes | Janino |
| Transformations haute performance | Janino |
| Scripts simples, prototypage | ScriptEngine |
| Formules configurables | Janino |
2. Architecture
2.1 Composants
eu.lmvi.socle.janino/
├── JaninoEngine.java # Moteur principal
├── JaninoWorker.java # Worker de gestion
├── JaninoScript.java # Script compilé
├── JaninoClassLoader.java # ClassLoader sécurisé
├── JaninoConfiguration.java # Configuration Spring
├── JaninoCompilationException.java # Exception
└── interfaces/
├── Calculator.java # Interface calculateur
├── Executable.java # Interface exécutable
├── Validator.java # Interface validateur
└── ValidationResult.java # Résultat validation
2.2 Diagramme de classes
┌───────────────────────┐
│ JaninoWorker │ (implements Worker)
│ @Component │
├───────────────────────┤
│ - janinoEngine │
│ - config │
│ - techDb │
│ - supervisor │
├───────────────────────┤
│ + execute() │
│ + compileScript() │
│ + forceReload() │
│ + getEngine() │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ JaninoEngine │
│ @Component │
├───────────────────────┤
│ - scriptCache │
│ - classLoader │
├───────────────────────┤
│ + compile() │
│ + execute() │
│ + reload() │
│ + getStats() │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ JaninoScript │
├───────────────────────┤
│ - name │
│ - compiledClass │
│ - executionCount │
│ - avgExecutionTimeNs │
└───────────────────────┘
3. Configuration
3.1 application.yml
socle:
janino:
# Activer Janino (défaut: false)
enabled: ${JANINO_ENABLED:false}
# Répertoire des scripts Java
scripts-path: ${JANINO_SCRIPTS_PATH:./repository/scripts/java}
# Intervalle de rechargement (défaut: 5 minutes)
reload-interval-ms: ${JANINO_RELOAD_INTERVAL:300000}
# Nombre max de classes en cache
max-cached-classes: ${JANINO_MAX_CACHED:100}
# Sécurité
security:
# Packages bloqués dans les scripts
blocked-packages:
- java.io
- java.net
- java.lang.reflect
- java.lang.invoke
- sun.
- com.sun.
# Timeout d'exécution max
max-execution-time-ms: ${JANINO_MAX_EXEC_TIME:5000}
3.2 Variables d’environnement
| Variable | Description | Défaut |
|---|---|---|
JANINO_ENABLED |
Activer Janino | false |
JANINO_SCRIPTS_PATH |
Répertoire scripts | ./repository/scripts/java |
JANINO_RELOAD_INTERVAL |
Intervalle reload (ms) | 300000 |
JANINO_MAX_CACHED |
Max classes en cache | 100 |
JANINO_MAX_EXEC_TIME |
Timeout exécution (ms) | 5000 |
4. Interfaces de Scripts
Les scripts Java doivent implémenter une des interfaces suivantes :
4.1 Calculator
Pour les calculs (frais, taxes, conversions).
package eu.lmvi.socle.janino.interfaces;
public interface Calculator<T> {
T calculate(Map<String, Object> context);
}
Exemple :
import eu.lmvi.socle.janino.interfaces.Calculator;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
public class FeeCalculator implements Calculator<BigDecimal> {
private static final BigDecimal FEE_RATE = new BigDecimal("0.0026");
@Override
public BigDecimal calculate(Map<String, Object> context) {
BigDecimal amount = (BigDecimal) context.get("amount");
String side = (String) context.get("side");
BigDecimal rate = "MAKER".equals(side)
? new BigDecimal("0.0016")
: FEE_RATE;
return amount.multiply(rate).setScale(8, RoundingMode.HALF_UP);
}
}
4.2 Executable
Pour les exécutions génériques.
package eu.lmvi.socle.janino.interfaces;
public interface Executable {
Object execute(Map<String, Object> context);
}
Exemple :
import eu.lmvi.socle.janino.interfaces.Executable;
import java.util.Map;
public class OrderProcessor implements Executable {
@Override
public Object execute(Map<String, Object> context) {
String orderId = (String) context.get("orderId");
Double amount = (Double) context.get("amount");
// Logique de traitement...
return Map.of(
"status", "PROCESSED",
"orderId", orderId,
"processedAmount", amount * 0.99
);
}
}
4.3 Validator
Pour les validations métier.
package eu.lmvi.socle.janino.interfaces;
public interface Validator {
ValidationResult validate(Object input);
}
Exemple :
import eu.lmvi.socle.janino.interfaces.Validator;
import eu.lmvi.socle.janino.interfaces.ValidationResult;
import java.math.BigDecimal;
public class OrderValidator implements Validator {
private static final BigDecimal MIN_AMOUNT = new BigDecimal("10.00");
private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000.00");
@Override
public ValidationResult validate(Object input) {
Order order = (Order) input;
ValidationResult result = new ValidationResult();
if (order.getAmount().compareTo(MIN_AMOUNT) < 0) {
result.addError("AMOUNT_TOO_LOW",
"Amount must be >= " + MIN_AMOUNT);
}
if (order.getAmount().compareTo(MAX_AMOUNT) > 0) {
result.addError("AMOUNT_TOO_HIGH",
"Amount must be <= " + MAX_AMOUNT);
}
if (order.getPair() == null || order.getPair().isEmpty()) {
result.addError("INVALID_PAIR", "Trading pair is required");
}
return result;
}
}
5. Utilisation
5.1 Structure des scripts
repository/scripts/java/
├── fees/
│ ├── KrakenFeeCalculator.java
│ ├── BinanceFeeCalculator.java
│ └── CryptoComFeeCalculator.java
├── validators/
│ ├── OrderValidator.java
│ └── AmountValidator.java
└── processors/
├── OrderProcessor.java
└── TradeProcessor.java
5.2 Injection dans un Worker
@Component
public class TradingWorker implements Worker {
@Autowired
private JaninoWorker janinoWorker;
@Override
public void doWork() {
// Exécuter un calculateur
Map<String, Object> context = Map.of(
"amount", new BigDecimal("1000.00"),
"side", "TAKER"
);
BigDecimal fee = janinoWorker.execute(
"KrakenFeeCalculator",
context,
BigDecimal.class
);
log.info("Fee calculated: {}", fee);
}
}
5.3 Compilation à la volée
@Autowired
private JaninoWorker janinoWorker;
public void compileCustomScript() {
String source = """
import eu.lmvi.socle.janino.interfaces.Calculator;
import java.math.BigDecimal;
import java.util.Map;
public class CustomCalculator implements Calculator<BigDecimal> {
@Override
public BigDecimal calculate(Map<String, Object> context) {
BigDecimal value = (BigDecimal) context.get("value");
return value.multiply(new BigDecimal("1.05"));
}
}
""";
janinoWorker.compileScript("CustomCalculator", source);
// Exécuter
BigDecimal result = janinoWorker.execute(
"CustomCalculator",
Map.of("value", new BigDecimal("100")),
BigDecimal.class
);
}
5.4 Accès direct au moteur
@Autowired
private JaninoWorker janinoWorker;
public void advancedUsage() {
JaninoEngine engine = janinoWorker.getEngine();
// Vérifier si un script est compilé
boolean ready = engine.isCompiled("FeeCalculator");
// Liste des scripts compilés
Set<String> scripts = engine.getCompiledScripts();
// Statistiques détaillées
Map<String, Map<String, Object>> stats = engine.getScriptStats();
// Forcer le rechargement
janinoWorker.forceReload();
}
6. Hot-Reload
Le JaninoWorker surveille automatiquement les modifications des fichiers .java dans le répertoire configuré.
Fonctionnement
- À chaque cycle (
reload-interval-ms), le worker scanne le répertoire - Pour chaque fichier modifié (timestamp changé), le script est recompilé
- Le nouveau bytecode remplace l’ancien dans le cache
- Les prochaines exécutions utilisent la nouvelle version
Logs
[exec:xxx][step:janino_reloaded] Reloaded script: FeeCalculator
[exec:xxx][step:janino_reload_cycle] Reload cycle completed (5 scripts)
Forcer le rechargement
// Recharger tous les scripts
janinoWorker.forceReload();
7. Securite
7.1 Validation des Imports (Source-Level)
Le JaninoEngine valide les imports avant la compilation pour bloquer l’acces aux packages dangereux. Cette approche (validation au niveau du code source) est plus compatible avec les fat-jars Spring Boot que la precedente approche ClassLoader.
| Package bloque | Raison |
|---|---|
java.io |
Acces fichiers |
java.net |
Acces reseau |
java.lang.reflect |
Reflection |
java.lang.invoke |
MethodHandles |
sun.* |
Classes internes |
com.sun.* |
Classes internes |
7.2 Fonctionnement
Source Java → validateSourceSecurity() → Compilation Janino → Execution
↓
Analyse des imports
↓
Blocage si package interdit
Le moteur analyse les declarations import dans le code source et rejette le script si un package bloque est detecte.
7.3 Tentative d’acces bloque
// Ce script sera bloque AVANT compilation
import java.io.File; // BLOQUE!
public class MaliciousScript implements Executable {
@Override
public Object execute(Map<String, Object> context) {
File file = new File("/etc/passwd");
return null;
}
}
Erreur :
JaninoCompilationException: Security violation in script 'MaliciousScript':
Import of blocked package 'java.io' is not allowed. Blocked import: java.io.File
7.4 Configuration personnalisee
socle:
janino:
security:
blocked-packages:
- java.io
- java.net
- java.lang.reflect
- java.lang.invoke
- sun.
- com.sun.
- com.mycompany.internal # Packages internes custom
8. Métriques et Monitoring
8.1 Stats du Worker
Map<String, Object> stats = janinoWorker.getStats();
// Résultat:
{
"name": "janino_worker",
"running": true,
"healthy": true,
"scripts_path": "./repository/scripts/java",
"reload_interval_ms": 300000,
"reload_count": 15,
"last_reload_at": "2026-01-17T10:30:00Z",
"scripts_compiled": 5,
"compilation_count": 8,
"compilation_errors": 0,
"execution_count": 1234,
"execution_errors": 2
}
8.2 Stats par script
Map<String, Map<String, Object>> scriptStats = janinoWorker.getEngine().getScriptStats();
// Résultat:
{
"KrakenFeeCalculator": {
"class": "KrakenFeeCalculator",
"compiled_at": 1705487400000,
"execution_count": 500,
"avg_execution_us": 12
},
"BinanceFeeCalculator": {
"class": "BinanceFeeCalculator",
"compiled_at": 1705487400000,
"execution_count": 300,
"avg_execution_us": 8
}
}
8.3 TechDB
Le worker persiste ses stats dans la table janino_stats :
SELECT * FROM janino_stats;
-- worker_name | scripts_count | compilation_count | execution_count | reload_count | last_reload
-- janino_worker | 5 | 8 | 1234 | 15 | 2026-01-17 10:30:00
9. Comparaison avec ScriptEngine
| Critère | ScriptEngine | JaninoEngine |
|---|---|---|
| Langages | JavaScript, BeanShell | Java pur |
| Exécution | Interprétée | Compilée (bytecode) |
| Performance | ~1000x plus lent | Native JVM |
| Typage | Dynamique | Statique |
| IDE Support | Limité | Complet (Java) |
| Debug | Difficile | Standard Java |
| Hot-reload | Non | Oui |
| Sécurité | Sandbox complexe | ClassLoader isolation |
| Courbe apprentissage | Nouveau langage | Java existant |
Quand utiliser quoi ?
| Situation | Recommandation |
|---|---|
| Calculs critiques (frais, taxes) | JaninoEngine |
| Validations complexes | JaninoEngine |
| Scripts simples, one-liners | ScriptEngine |
| Prototypage rapide | ScriptEngine |
| Logique métier complexe | JaninoEngine |
| Transformations JSON basiques | ScriptEngine |
10. Dépannage
10.1 Script non compilé
Erreur :
IllegalStateException: Script not compiled: MyScript
Solution :
- Vérifier que le fichier existe dans
scripts-path - Vérifier l’extension
.java - Consulter les logs pour les erreurs de compilation
10.2 Erreur de compilation
Erreur :
JaninoCompilationException: Failed to compile script: MyScript
Solution :
- Vérifier la syntaxe Java
- Vérifier les imports
- Vérifier que la classe implémente une interface valide
10.3 Classe non trouvée
Erreur :
Cannot find class name in source
Solution :
- Le source doit contenir
public class NomDeLaClasse - Le nom de la classe doit correspondre au nom du fichier
10.4 Package bloque
Erreur :
JaninoCompilationException: Security violation in script 'MyScript':
Import of blocked package 'java.io' is not allowed
Solution :
- Utiliser uniquement les packages autorises
- Si necessaire, modifier la config
blocked-packages
10.5 Cannot load simple types (Fat-Jar Spring Boot)
Erreur :
org.codehaus.commons.compiler.CompileException: Cannot load simple types
Cause : Ce probleme survient dans les fat-jars Spring Boot car le classloader personnalise casse la resolution des modules Java 9+.
Solution : Cette erreur a ete corrigee dans le socle V4. Le JaninoEngine utilise maintenant le context classloader directement :
// JaninoEngine.java - ligne 117-121
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
if (contextLoader == null) {
contextLoader = getClass().getClassLoader();
}
compiler.setParentClassLoader(contextLoader);
Si vous rencontrez encore cette erreur, verifiez que vous utilisez la derniere version du socle.
10.6 Assignment conversion not possible from Object
Erreur :
Line 35, Column 41: Assignment conversion not possible from type "java.lang.Object" to type "java.math.BigDecimal"
Cause : Janino ne supporte pas l’inference de type pour Map.get().
Solution : Ajouter un cast explicite :
// AVANT (erreur)
BigDecimal rate = myMap.get(key);
// APRES (correct)
BigDecimal rate = (BigDecimal) myMap.get(key);
10.7 Invalid escape sequence
Erreur :
Line 27, Column 35: Invalid escape sequence
Cause : Les sequences d’echappement regex (\s, \d, \{) doivent etre double-echappees.
Solution :
// AVANT (erreur)
Pattern p = Pattern.compile("\s+");
// APRES (correct)
Pattern p = Pattern.compile("\\s+");
10.8 Invocation of static interface methods
Erreur :
Invocation of static interface methods only available for target version 8+
Cause : Janino ne supporte pas Map.of(), Set.of(), List.of() (Java 9+).
Solution : Utiliser des blocs static :
// AVANT (erreur)
private static final Map<String, String> DATA = Map.of("a", "b");
// APRES (correct)
private static final Map<String, String> DATA = new HashMap<>();
static {
DATA.put("a", "b");
}
11. Bonnes Pratiques
DO
- Implémenter une des interfaces (
Calculator,Executable,Validator) - Utiliser des types explicites (pas de
var) - Gérer les exceptions dans le script
- Tester les scripts avant déploiement
- Utiliser des noms de classes descriptifs
DON’T
- Ne pas utiliser
java.io,java.net,java.lang.reflect - Ne pas stocker d’état entre les exécutions (stateless)
- Ne pas faire d’opérations bloquantes longues
- Ne pas utiliser de dépendances externes non disponibles
12. Limitations Janino et Compatibilite
Janino est un compilateur Java simplifie qui ne supporte pas toutes les fonctionnalites du langage Java moderne. Cette section documente les limitations et les solutions.
12.1 Fonctionnalites Non Supportees
| Fonctionnalite | Version Java | Statut Janino |
|---|---|---|
Methodes generiques <T> |
Java 5+ | NON SUPPORTE |
Map.of(), Set.of(), List.of() |
Java 9+ | NON SUPPORTE |
switch expressions |
Java 14+ | NON SUPPORTE |
var (inference de type) |
Java 10+ | NON SUPPORTE |
| Records | Java 16+ | NON SUPPORTE |
| Pattern matching | Java 16+ | NON SUPPORTE |
Text blocks """ |
Java 15+ | NON SUPPORTE |
12.2 Solutions et Contournements
A. Pas de methodes generiques
// INTERDIT - Ne compile pas
private <T extends Number> T extractValue(String json, String key, Class<T> type) {
// ...
}
// CORRECT - Methodes specifiques par type
private Integer extractIntValue(String json, String key) {
// implementation pour Integer
}
private Double extractDoubleValue(String json, String key) {
// implementation pour Double
}
B. Pas de Map.of() / Set.of() / List.of()
// INTERDIT - Ne compile pas
private static final Map<String, BigDecimal> RATES = Map.of(
"FR", new BigDecimal("0.015"),
"DE", new BigDecimal("0.018")
);
// CORRECT - Bloc static avec HashMap
private static final Map<String, BigDecimal> RATES = new HashMap<>();
static {
RATES.put("FR", new BigDecimal("0.015"));
RATES.put("DE", new BigDecimal("0.018"));
}
// CORRECT - Pour Set
private static final Set<String> COUNTRIES = new HashSet<>();
static {
COUNTRIES.add("FR");
COUNTRIES.add("DE");
}
C. Cast explicite pour Map.get()
// INTERDIT - "Assignment conversion not possible from Object to BigDecimal"
BigDecimal rate = RATES.get(country);
// CORRECT - Cast explicite
BigDecimal rate = (BigDecimal) RATES.get(country);
if (rate == null) rate = BigDecimal.ZERO;
D. Pas de switch expressions
// INTERDIT - Ne compile pas
String result = switch (status) {
case "A" -> "Active";
case "I" -> "Inactive";
default -> "Unknown";
};
// CORRECT - Switch statement classique
String result;
switch (status) {
case "A": result = "Active"; break;
case "I": result = "Inactive"; break;
default: result = "Unknown";
}
E. Double echappement des regex
// INTERDIT - "Invalid escape sequence"
Pattern p = Pattern.compile("\s+");
Pattern p2 = Pattern.compile("\{.*\}");
// CORRECT - Double echappement
Pattern p = Pattern.compile("\\s+");
Pattern p2 = Pattern.compile("\\{.*\\}");
12.3 Template de Script Compatible
Voici un template de script qui fonctionne avec Janino :
import eu.lmvi.socle.janino.interfaces.Calculator;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MyCalculator implements Calculator<BigDecimal> {
// Collections statiques avec bloc static
private static final Map<String, BigDecimal> RATES = new HashMap<>();
private static final Set<String> VALID_CODES = new HashSet<>();
static {
RATES.put("A", new BigDecimal("0.10"));
RATES.put("B", new BigDecimal("0.20"));
VALID_CODES.add("X");
VALID_CODES.add("Y");
}
@Override
public BigDecimal calculate(Map<String, Object> context) {
// Extraction avec cast explicite
BigDecimal amount = extractAmount(context.get("amount"));
String code = (String) context.get("code");
if (code == null) code = "A";
// Acces Map avec cast
BigDecimal rate = (BigDecimal) RATES.get(code);
if (rate == null) rate = new BigDecimal("0.15");
return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
// Methode d'extraction type-safe (pas de generiques)
private BigDecimal extractAmount(Object value) {
if (value == null) return BigDecimal.ZERO;
if (value instanceof BigDecimal) return (BigDecimal) value;
if (value instanceof Number) {
return BigDecimal.valueOf(((Number) value).doubleValue());
}
try {
return new BigDecimal(value.toString());
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
}
12.4 Checklist de Compatibilite
Avant de deployer un script Janino, verifiez :
- [ ] Pas de
<T>dans les signatures de methodes - [ ] Pas de
Map.of(),Set.of(),List.of() - [ ] Pas de
varpour les declarations - [ ] Pas de
switchexpressions (fleches->) - [ ] Cast explicite
(Type)pour tous lesMap.get() - [ ] Double echappement
\\dans les regex - [ ] Pas de text blocks
""" - [ ] Imports explicites (pas de wildcards
import java.util.*)
13. Exemple Complet
13.1 Script : TradingFeeCalculator.java
import eu.lmvi.socle.janino.interfaces.Calculator;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
import java.util.HashMap;
/**
* Calculateur de frais de trading multi-exchange.
* Compatible Janino (pas de switch expressions, pas de Map.of)
*/
public class TradingFeeCalculator implements Calculator<BigDecimal> {
// Taux MAKER par exchange
private static final Map<String, BigDecimal> MAKER_RATES = new HashMap<>();
// Taux TAKER par exchange
private static final Map<String, BigDecimal> TAKER_RATES = new HashMap<>();
static {
MAKER_RATES.put("KRAKEN", new BigDecimal("0.0016"));
MAKER_RATES.put("BINANCE", new BigDecimal("0.0010"));
MAKER_RATES.put("COINBASE", new BigDecimal("0.0040"));
TAKER_RATES.put("KRAKEN", new BigDecimal("0.0026"));
TAKER_RATES.put("BINANCE", new BigDecimal("0.0010"));
TAKER_RATES.put("COINBASE", new BigDecimal("0.0060"));
}
private static final BigDecimal DEFAULT_RATE = new BigDecimal("0.0025");
@Override
public BigDecimal calculate(Map<String, Object> context) {
String exchange = (String) context.get("exchange");
BigDecimal amount = (BigDecimal) context.get("amount");
String side = (String) context.get("side");
if (exchange == null) exchange = "DEFAULT";
if (side == null) side = "TAKER";
BigDecimal feeRate = getFeeRate(exchange.toUpperCase(), side.toUpperCase());
return amount.multiply(feeRate).setScale(8, RoundingMode.HALF_UP);
}
private BigDecimal getFeeRate(String exchange, String side) {
boolean isMaker = "MAKER".equals(side);
// Cast explicite requis par Janino
BigDecimal rate;
if (isMaker) {
rate = (BigDecimal) MAKER_RATES.get(exchange);
} else {
rate = (BigDecimal) TAKER_RATES.get(exchange);
}
if (rate == null) {
rate = DEFAULT_RATE;
}
return rate;
}
}
13.2 Worker utilisant le script
Note : Ce Worker est du code Java standard compile par javac/Maven, pas un script Janino. Il peut donc utiliser
List.of(),Map.of()et les fonctionnalites Java modernes.
@Component
public class FeeWorker implements Worker {
private static final Logger log = LoggerFactory.getLogger(FeeWorker.class);
@Autowired
private JaninoWorker janinoWorker;
@Override
public String getName() {
return "fee-worker";
}
@Override
public void doWork() {
// Calculer les frais pour différents exchanges
List<String> exchanges = List.of("KRAKEN", "BINANCE", "COINBASE");
BigDecimal amount = new BigDecimal("10000.00");
for (String exchange : exchanges) {
Map<String, Object> context = Map.of(
"exchange", exchange,
"amount", amount,
"side", "TAKER"
);
BigDecimal fee = janinoWorker.execute(
"TradingFeeCalculator",
context,
BigDecimal.class
);
log.info("Fee for {} on amount {}: {}",
exchange, amount, fee);
}
}
// ... autres méthodes Worker
}
14. References
- 05-WORKERS – Section 13 JaninoWorker
- Specification Janino V2
- Janino Official

Laisser un commentaire