Esigenza Reale
Ridurre il Time To First Byte (TTFB) servendo parti comuni del layout istantaneamente.
Analisi Tecnica
Problema: Il motore di template ri-elabora frammenti identici (es. header/footer) per ogni singola richiesta HTTP.
Perché: Implemento il caching dei frammenti. Ho scelto un approccio basato su cache distribuita per garantire che il lavoro di rendering venga fatto una sola volta per tutti i nodi del cluster.
Esempio Implementativo
1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* Strategia 1: caching a livello di TemplateResolver (frammenti completamente
statici). */
@Bean public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new
SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setCacheable(true);
resolver.setCacheTTLMs(3_600_000L);
// Cache di 1 ora: per header/footer che cambiano raramente
return resolver;
}
/* Strategia 2: caching del frammento HTML già renderizzato con Spring Cache +
Redis. Uso questa strategia per frammenti semi-dinamici (es. menu che
dipende dal ruolo ma non cambia per ogni richiesta). */
@Service public class FragmentCacheService {
private final TemplateEngine templateEngine;
private final ApplicationContext applicationContext;
@Cacheable( value = "rendered-fragments", key = "#fragmentName + ':' +
#cacheKey", unless = "#result == null" ) public String
renderFragment(String fragmentName, String cacheKey, Map<String, Object>
variables) {
// Creo un contesto Thymeleaf con le variabili fornite Context context =
new Context()
;
context.setVariables(variables);
// Processo solo il frammento specificato, non l'intero template
return templateEngine.process(fragmentName, context);
}
@CacheEvict(value = "rendered-fragments", allEntries = true) public void
invalidateAllFragments() {
// Invalido quando i dati di navigazione cambiano (es. nuova voce di
menu aggiunta)
}
}
/* Configurazione Redis per la cache distribuita: */
@Configuration
@EnableCaching public class CacheConfig {
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory)
{
RedisCacheConfiguration fragmentConfig =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()));
return RedisCacheManager.builder(factory)
.withCacheConfiguration("rendered-fragments", fragmentConfig)
.build();
}
}
/* Controller che usa il fragment caching: */
@Controller public class PageController {
@Autowired private FragmentCacheService fragmentCache;
@GetMapping("/dashboard") public String dashboard(Model model,
Authentication auth) {
// Il navbar viene renderizzato una sola volta per ruolo e cachato in
Redis String navbarHtml = fragmentCache.renderFragment(
"fragments/navbar", // Template del frammento
auth.getAuthorities().toString(), // Chiave di cache basata sul
ruolo Map.of("user", auth.getName(), "roles", auth.getAuthorities())
)
;
model.addAttribute("navbarHtml", navbarHtml);
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
}
/* Nel template dashboard.html: inietto l'HTML pre-renderizzato senza
re-processing. */
// <div th:utext="$
{
navbarHtml
}
"></div> <!-- th:utext per HTML raw non escaped -->
// <div th:replace="~
{
fragments/dashboard-content :: content
}
"></div>
/* In application.properties: */
// spring.thymeleaf.cache=true # Obbligatorio in produzione //
spring.cache.type=redis // spring.data.redis.host=redis-cluster.internal //
spring.data.redis.port=6379