Releases: crysknife-io/crysknife
Release list
Crysknife 0.11 Release Notes
Crysknife 0.11 is a major feature release that brings the framework closer to the Jakarta CDI specification,
adds new UI modules for REST, WebSocket, security and data binding, and includes significant
improvements to the event system, navigation, internationalization, and template engine.
Minimum requirements: Java 21, Maven 3.9
CDI Core
1. Jakarta CDI Interceptors
Full support(well, what can be done in js env) for Jakarta Interceptors specification: @InterceptorBinding, @Interceptor, and @AroundInvoke.
Interceptors allow cross-cutting concerns (logging, metrics, authorization) to be applied declaratively
to bean methods without modifying business logic.
// Define an interceptor binding
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Logged {}
// Implement the interceptor
@ApplicationScoped
@Interceptor
@Logged
public class LoggingInterceptor {
@AroundInvoke
public Object log(InvocationContext ctx) throws Exception {
String method = ctx.getMethod().getName();
console.log(">> " + method);
Object result = ctx.proceed();
console.log("<< " + method);
return result;
}
}
// Apply to a bean — per method or on the entire class
@ApplicationScoped
public class OrderService {
@Logged
public void placeOrder(Order order) { ... }
public void cancel(Order order) { ... } // not intercepted
}2. Jakarta CDI Decorators
Support for the Decorator pattern from the CDI specification: @Decorator, @Delegate, and @Priority
for ordering. Decorators wrap beans transparently while preserving the contract of the decorated interface.
public interface Greeter {
String greet(String name);
}
@ApplicationScoped
public class SimpleGreeter implements Greeter {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
// Decorator wraps the original bean
@ApplicationScoped
@Decorator
@Priority(1000)
public class UpperCaseGreeterDecorator implements Greeter {
@Inject @Delegate
Greeter delegate;
public String greet(String name) {
return delegate.greet(name).toUpperCase();
}
}Multiple decorators can be chained via @Priority ordering — higher priority = outermost wrapper:
@Decorator @Priority(1000)
public class BracketFormatter implements Formatter { ... } // inner
@Decorator @Priority(2000)
public class StarFormatter implements Formatter { ... } // outer
// Result: *[hello]*3. Event System Rewrite
The event system has been rewritten around ObserverRegistry with type-hierarchy dispatch.
Firing ChildEvent extends BaseEvent now correctly notifies observers of BaseEvent.
Non-@Dependent scoped beans with @Observes are lazily activated on first event delivery.
public class BaseEvent {}
public class OrderEvent extends BaseEvent {}
@ApplicationScoped
public class AuditObserver {
// Receives both BaseEvent and OrderEvent (type-hierarchy dispatch)
public void onEvent(@Observes BaseEvent event) {
audit(event);
}
}
@Singleton
public class LazyMetricsObserver {
// Bean is not instantiated until the first MetricsEvent fires
public void onMetrics(@Observes MetricsEvent event) {
record(event);
}
}New Modules
4. REST Client (Caller<T>)
New module ui/rest provides type-safe REST client generation. Define a JAX-RS interface, inject
Caller<T>, and make HTTP calls — the framework generates the implementation at compile time.
REST caller and JSON mapper generation are fully integrated into the crysknife processor —
users no longer need to add jakarta-rest:processor or json-mapper:processor as separate
annotation processor dependencies. Everything is handled by crysknife-ui-rest-generator.
Basic usage
// Define REST interface with standard JAX-RS annotations
@Path("/items")
public interface ItemService {
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Item> listItems();
@GET @Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
Item getItem(@PathParam("id") long id);
@POST @Consumes(MediaType.APPLICATION_JSON)
Item createItem(Item item);
@PUT @Path("/{id}") @Consumes(MediaType.APPLICATION_JSON)
Item updateItem(@PathParam("id") long id, Item item);
@DELETE @Path("/{id}")
Item deleteItem(@PathParam("id") long id);
@PATCH @Path("/{id}") @Consumes(MediaType.APPLICATION_JSON)
Item patchItem(@PathParam("id") long id, Item item);
@GET @Path("/search")
List<Item> searchItems(@QueryParam("q") String query);
@GET @Path("/{id}/header")
Item getItemWithHeader(@PathParam("id") long id, @HeaderParam("X-Custom-Header") String header);
}
// Configure base URL via CDI producer
@ApplicationScoped
public class RestDemoConfig {
@Produces
@Named("api")
public RestConfig apiConfig() {
return RestConfig.builder()
.baseUrl("https://api.example.com")
.build();
}
}
// Inject and use
@Inject @Named("api")
Caller<ItemService> itemService;
public void loadItem() {
itemService
.onError((response, throwable) -> console.error("Failed: " + response.getStatusCode()))
.call(result -> {
Item item = (Item) result;
console.log(item.getName());
})
.getItem(1);
}Full response access (body + headers + status code):
itemService
.call((body, response) -> {
Item item = (Item) body;
int status = response.getStatusCode();
})
.getItem(1);Bearer token authentication
Static token or dynamic supplier (re-evaluated per request):
// Static token
RestConfig.builder()
.baseUrl("https://api.example.com")
.bearerToken("my-secret-token")
.build();
// Dynamic token — supplier called on every request
RestConfig.builder()
.baseUrl("https://api.example.com")
.bearerToken(() -> currentToken)
.build();Retry policy
Automatic retry with configurable conditions — for example, retry on 401 with token refresh:
RestConfig.builder()
.baseUrl("https://api.example.com")
.bearerToken(() -> currentToken)
.retryPolicy(RetryPolicy.builder()
.maxRetries(1)
.delayMs(0)
.condition((response, error, attempt) ->
response != null && response.getStatusCode() == 401)
.build())
.responseFilter((reqCtx, respCtx) -> {
if (respCtx.getStatusCode() == 401) {
currentToken = refreshToken();
}
})
.build();Request and response filters
// Request filter — add headers, modify request, or abort
RestConfig.builder()
.requestFilter(ctx ->
ctx.getHeaders().put("X-Custom-Header", "value"))
.build();
// Abort request without hitting the server
RestConfig.builder()
.requestFilter(ctx -> ctx.abortWith(
new RestResponse(403, "Forbidden", "{}", Collections.emptyMap())))
.build();
// Response filter — inspect response
RestConfig.builder()
.responseFilter((reqCtx, respCtx) ->
console.log("Status: " + respCtx.getStatusCode()))
.build();Exception mapper
Map HTTP error codes to typed exceptions:
RestConfig.builder()
.exceptionMapper(response -> {
if (response.getStatusCode() == 404) {
return new NotFoundException(response);
}
if (response.getStatusCode() == 401) {
return new UnauthorizedException(response);
}
return null; // fallback to RestException
})
.build();Promise API
Generated _RestCaller class provides a promise-based alternative:
ItemService_RestCaller caller = (ItemService_RestCaller) itemService;
caller.promiseGetItem(1)
.then(item -> console.log(item.getName()))
.catchError(error -> console.error(error.getMessage()));Keycloak OIDC integration
The combination of dynamic bearer token, retry policy, and response filter provides a clean
integration pattern with Keycloak (or any OIDC provider). The REST client handles the full
token lifecycle — initial authentication, automatic refresh on 401, and per-request token
injection — without any Keycloak-specific dependencies on the client side.
// Shared REST interface — used by both client (J2CL) and server (Quarkus)
@Path("/api/secure")
@Produces(MediaType.APPLICATION_JSON)
public interface SecureItemService {
@GET @Path("/item")
SecureItem getSecureItem();
}// Server side — Quarkus resource with @RolesAllowed
@Path("/api/secure")
@Produces(MediaType.APPLICATION_JSON)
public class SecureItemResource {
@GET @Path("/item")
@RolesAllowed("user")
public SecureItem getSecureItem() {
return new SecureItem(1, "protected-data");
}
}// Client side — automatic token refresh on 401
String[] currentToken = { keycloakToken };
Caller<SecureItemService> caller = new SecureItemService_RestCaller(
RestConfig.builder()
.baseUrl(baseUrl)
.bearerToken(() -> currentToken[0])
.retryPolicy(RetryPolicy.builder()
.maxRetries(1)
.delayMs(0)
.condition((response, error, attempt) ->
response != null && response.getStatusCode() == 401)
.build())
.responseFilter((reqCtx, respCtx) -> {
if (respCtx.getStatusCode() == 401) {
currentToken[0] = refreshTokenFromKeycloak();
}
})
...v0.8
What's Changed
- updated to the latest j2cl
- elemental2 updated to 1.2.3
- deps updated to the latest
Full Changelog: v0.7...v0.8
0.7 Release
- bump elemental2 to 1.2.1
- bump to latest j2cl
0.6 Release
It's the first stable and feature-complete release; see README for the features.
0.4 Release
- Removed gwtproject dependencies
- Removed gwt2 related stuff
- Added @PreDestroy annotation
- Navigation has been reimplemented on the top of the elemental2 and moved to the ui from legacy modules
- Added @PageHidden @PageHiding @PageShowing and @PageShown
- Bugfix
first stable release 0.3
@Inject,@Singletonand@Dependentscopes- Lazy fields and constructor injections
@PostConstruct@Namedqualifiers and@Qualifierannotations@Producesfor custom objects like Elemental2 widgets@Typed@Specializes- HTML templates and events binding
- Navigation and many other features
v0.1
-- all components updated to elemental2 1.1.0
-- all components updated to gwt modules 1.0.0-RC1
-- elemento updated to 1.0.1 version
Because of gwt-i18n status, all related modules temporary disable
Crysknife is published to sonatype