Use case
I'd like to add programmatically an externally provided annotation named: #Trace
to all public methods in the spring-boot project
that are in a class annotated with #Controller
only within a particular package (com.example.apectitddemo.controller)
only if the method doesn't have a different custom annotation already applied, f.e. #Disable
Thanks to the above criteria, each newly added method to the project that meets them all will be #Trace annotated dynamically without any additional developer action, which is the main goal here.
My approach
I used Aspectj's ITD (inter type declaration) for this but it fulfills only 1st requirement and have no idea how to customize it for 2nd, 3rd and 4th.
Tried several ways commented out in the below code snipped.
TracingAspect.aj:
package com.example.apectitddemo.aspect;
public aspect TracingAspect {
declare #method : public * *(..) : #Trace;
//[INFO] 'public void com.example.apectitddemo.controller.ControllerPing.ping()' (ControllerPing.java) is annotated with #Trace method annotation from 'com.example.apectitddemo.aspect.TracingAspect' (TracingAspect.aj)
// declare #method : public * ((#Controller *)).*(..) : #Trace;
// declare #method : public * ((#Controller *)).*(..) && !#Disabled : #Trace;
// declare #method : public com.example.apectitddemo.controller.* :#Trace;
// declare #method : public * com.example.apectitddemo.controller+ : #Trace;
// declare #method : public * *(com.example.apectitddemo.controller.*) : #Trace;
// declare #method : public * controller..* : #Trace;
// declare #method : public * *(..) : #Trace;
}
BTW is it possible to use pure java here (TracingAspect.java) and not as .aj file?
ControllerPing.java (sample method which should be annotated by an aspect)
package com.example.apectitddemo.controller
#Controller
public class ControllerPing {
//#Trace annotation should be added here by ITD
public void ping() {
log.info("ok");
}
#Disable
public void pingDisabled() {
log.info("ok");
}
}
Misc
I was searching the internet but haven't found much documentation and even couldn't encounter any other code samples except below. The above solution is based on this finding:
how to write an aspectj itd to add an annotation to a method?
Other pages found, related:
https://www.eclipse.org/aspectj/doc/released/adk15notebook/ataspectj-itds.html
http://kto.web.elte.hu/hu/oktatas/aop_en.pdf
samples are empty :/ https://www.eclipse.org/aspectj/doc/released/examples/
Maybe there is another better way to complete the requirements?
This is a native AspectJ example. You can run it completely without Spring or from within a Spring application - up to you:
Annotations:
package de.scrum_master.stackoverflow.q73270343;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Retention(RUNTIME)
#Target({ METHOD })
public #interface Trace {}
package de.scrum_master.stackoverflow.q73270343;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Retention(RUNTIME)
#Target({ METHOD })
public #interface Disable {}
Non-controller class (negative test):
package de.scrum_master.stackoverflow.q73270343;
public class NoControllerPing {
// #Trace annotation should NOT be added here by ITD
public void ping() {
System.out.println("No controller ping");
}
#Disable
public void pingDisabled() {
System.out.println("No controller pingDisabled");
}
}
Controller class and driver application:
package de.scrum_master.stackoverflow.q73270343;
import org.springframework.stereotype.Controller;
#Controller
public class ControllerPing {
// #Trace annotation should be added here by ITD
public void ping() {
System.out.println("Controller ping");
}
#Disable
public void pingDisabled() {
System.out.println("Controller pingDisabled");
}
public static void main(String[] args) {
new ControllerPing().ping();
new ControllerPing().pingDisabled();
new NoControllerPing().ping();
new NoControllerPing().pingDisabled();
}
}
Native syntax AspectJ aspect:
package de.scrum_master.stackoverflow.q73270343;
public aspect TracingAspect {
declare #method : !#Disable public !static * (#org.springframework.stereotype.Controller *..*).*(..) : #Trace;
before() : #annotation(Trace) && execution(* *(..)) {
System.out.println(thisJoinPoint);
}
}
Console log:
execution(void de.scrum_master.stackoverflow.q73270343.ControllerPing.ping())
Controller ping
Controller pingDisabled
No controller ping
No controller pingDisabled
See AspectJ manual.
BTW is it possible to use pure java here (TracingAspect.java) and not as .aj file?
No, it is not. Quote from the AspectJ manual:
Inter-type declarations are challenging to support using an annotation style. For code style aspects compiled with the ajc compiler, the entire type system can be made aware of inter-type declarations (new supertypes, new methods, new fields) and the completeness and correctness of it can be guaranteed. Achieving this with an annotation style is hard because the source code may simply be compiled with javac where the type system cannot be influenced and what is compiled must be 'pure java'.
You only have #DeclareParents, #DeclareMixin, #DeclarePrecedence, #DeclareWarning, #DeclareError at your disposal. See also the AspectJ 5 quick reference, page 4.
Related
In json-b, I can use a custom PropertyVisibilityStrategy. What is the behaviour for quarkus if I use the following:
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.json.bind.config.PropertyVisibilityStrategy;
/**
* Enables private property visibility
* and disables method visibility for JSON-B processings.
*/
public final class PrivateVisibilityStrategy implements PropertyVisibilityStrategy {
#Override
public boolean isVisible(Field field) {
return true;
}
#Override
public boolean isVisible(Method method) {
return false;
}
}
and then on a class I can use
#JsonbVisibility(value = PrivateVisibilityStrategy.class)
public class User {
// only fields are used for json mappings because of "PrivateVisibilityStrategy"
..
}
Questions:
Will quarkus use reflection for such cases (because it must use field access)?
If yes, should I avoid such cases in quarkus to avoid reflection to improve performance?
Should I always avoid field access in quarkus by reflections? Or does quarkus use java.lang.invoke.MethodHandles for such cases behind the scene?
I was able to do this using a class like this:
#Singleton
public class JsonConfigurator implements JsonbConfigCustomizer {
public void customize(JsonbConfig config) {
config.withPropertyVisibilityStrategy(new PrivateVisibilityStrategy());
}
}
PrivateVisibilityStrategyis just like you posted.
The Mirconaut docs on JDBC repositories clearly tells us we have to create a test repository to test against another dialect. I think this will be manageable (e.g. Postgres for production and H2 for test).
The problem is I have to repeat my methods (e.g. find()) in the test repository. I have a book repository and a test repository:
#JdbcRepository(dialect = Dialect.POSTGRES)
interface BookRepository extends CrudRepository<Book, Long> {
Optional<Book> find(String title);
}
#JdbcRepository(dialect = Dialect.H2)
#Replaces(bean = BookRepository)
#Requires(env = ["test"])
interface TestBookRepository extends BookRepository {
// Optional<Book> find(String title);
// Required to make the find() method appear in the TestBookRepository
}
To make the find() method available in the TestBookRepository, I had to repeat the method (see commented line above).
Is there a better way to avoid repeating myself? The methods from the CrudRepository interface are available in the TestBookRepository without problems. Why is the find() method not treated the same?
BTW, I don't want to mock the test repository. I want to test the repository 'logic' injected by Micronaut-Data against an SQL database.
This is for Micronaut Data 1.0.0.M5, using Groovy for the source.
To make the find() method available in the TestBookRepository, I had
to repeat the method (see commented line above).
I cannot reproduce that behavior. In order for that to be the case I think the java compiler would need to have a bug in it that caused that.
See the project at https://github.com/jeffbrown/mikehoustonrepository.
https://github.com/jeffbrown/mikehoustonrepository/blob/82b8af568042c762a86cef9965e52fdc61053421/src/main/java/mikehoustonrepository/BookRepository.java
// src/main/java/mikehoustonrepository/BookRepository.java
package mikehoustonrepository;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.Optional;
#JdbcRepository(dialect = Dialect.POSTGRES)
public interface BookRepository extends CrudRepository<Book, Long> {
Optional<Book> find(String title);
}
https://github.com/jeffbrown/mikehoustonrepository/blob/82b8af568042c762a86cef9965e52fdc61053421/src/test/java/mikehoustonrepository/TestBookRepository.java
// src/test/java/mikehoustonrepository/TestBookRepository.java
package mikehoustonrepository;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
#JdbcRepository(dialect = Dialect.H2)
#Replaces(BookRepository.class)
public interface TestBookRepository extends BookRepository{}
https://github.com/jeffbrown/mikehoustonrepository/blob/82b8af568042c762a86cef9965e52fdc61053421/src/main/java/mikehoustonrepository/BookController.java
package mikehoustonrepository;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import java.util.Optional;
#Controller("/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
#Get("/")
public Iterable<Book> index() {
return bookRepository.findAll();
}
#Post("/{title}/{author}")
public Book create(String title, String author) {
return bookRepository.save(new Book(title, author));
}
#Get("/find/{title}")
public Optional<Book> findByTitle(String title) {
return bookRepository.find(title);
}
}
https://github.com/jeffbrown/mikehoustonrepository/blob/82b8af568042c762a86cef9965e52fdc61053421/src/test/java/mikehoustonrepository/BookControllerTest.java
package mikehoustonrepository;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
#MicronautTest
public class BookControllerTest {
#Inject
BookClient bookClient;
#Test
public void testFind() throws Exception {
Optional<Book> book = bookClient.find("The Nature Of Necessity");
assertFalse(book.isPresent());
bookClient.create("The Nature Of Necessity", "Alvin Plantinga");
book = bookClient.find("The Nature Of Necessity");
assertTrue(book.isPresent());
}
}
#Client(value="/", path = "/books")
interface BookClient {
#Post("/{title}/{author}")
Book create(String title, String author);
#Get("/")
List<Book> list();
#Get("/find/{title}")
Optional<Book> find(String title);
}
That test passes.
You can see that a different repository is being used for test (TestBookRepository) that is used for other environments (BookRepository).
I hope that helps.
You can utilise Micronaut environments to create different environment configuration for test and production
and configure respective datasource configuration in application-test.yml
and use that datasource for tests
Micronaut Environments from docs
After some more work, I found another way to solve the original problem. You can define a base interface class that just has the methods you need. Then implement concrete classes for the dialect(s) you need. This allows one type of DB for test and one for production.
interface OrderRepository extends BaseRepository, CrudRepository<Order, UUID> {
#Join(value = "product", type = Join.Type.LEFT_FETCH)
Optional<Order> findById(UUID uuid)
}
#JdbcRepository(dialect = Dialect.H2)
#Requires(env = ["test"])
interface OrderRepositoryH2 extends OrderRepository, CrudRepository<Order, UUID> {
}
#JdbcRepository(dialect = Dialect.POSTGRES)
#Requires(env = ["dev"])
interface OrderRepositoryPostgres extends OrderRepository, CrudRepository<Order, UUID> {
}
No methods are needed in the OrderRepositoryH2 interface. Micronaut-data uses the methods from the parent interface fine. The trick is to not use the #JdbcRepository annotation in the parent interface.
You can create any other dialects needed, but you have to make sure the #Requires annotation results in only one bean for any given mode.
I plan to use H2 for testing, with an option to use the Postgres dialect for special test runs when needed.
Sorry for any confusion on the question and comments.
(I decided to mark this as the answer since it solves the original problem).
I have some fields in a model that I only want to be returned when the logged in user has the role ROLE_ADMIN. I can use #JsonIgnore but that hides it for everyone. How can I make it hide dynamically?
You should use Jackson Json Views technology to acheive it - it allows to choose a different set of fields to be serialized programatically. It is also supported by Spring
Consider you have a class Model with two properties: commonField which should be available for everyone and secretField which should be available only for certain users. You should create an hierarchy of views (any classes would work) and specify which field is available in which view using #JsonView annotation
package com.stackoverflow.jsonview;
import com.fasterxml.jackson.annotation.JsonView;
public class Model {
public static class Public {}
public static class Secret extends Public {}
#JsonView(Public.class)
private String commonField;
#JsonView(Secret.class)
private String secretField;
public Model() {
}
public Model(String commonField, String secretField) {
this.commonField = commonField;
this.secretField = secretField;
}
public String getCommonField() {
return commonField;
}
public void setCommonField(String commonField) {
this.commonField = commonField;
}
public String getSecretField() {
return secretField;
}
public void setSecretField(String secretField) {
this.secretField = secretField;
}
}
Now you can specify the view you want to use in concrete ObjectMapper
package com.stackoverflow.jsonview;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import static org.junit.Assert.*;
/**
*/
public class ModelTest {
#Test
public void testSecretField() throws JsonProcessingException {
Model model = new Model("commonField","secretField");
assertEquals("{\"commonField\":\"commonField\",\"secretField\":\"secretField\"}", new ObjectMapper().writerWithView(Model.Secret.class).writeValueAsString(model));
assertEquals("{\"commonField\":\"commonField\"}", new ObjectMapper().writerWithView(Model.Public.class).writeValueAsString(model));
}
}
I am not sure if you can use declaratie approach to make spring choose the right view based on user role out of the box, so probably you will have to write some code like this:
#RequestMapping("/data")
public String getData(HttpServletRequest request) {
Model model = service.getModel();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper = request.isUserInRole("ROLE_ADMIN") ? objectMapper.writerWithView(Model.Secret.class) : objectMapper.writerWithView(Model.Public.class);
return objectMapper.writeValueAsString(model);
}
I solved this after literally a full month of trying various things. I'm working with Spring 4.3.1 and boot, with data being returned in Hal using a pagedrepository.
extend RepositoryRestMvcConfiguration as MyRepositoryRestMvcConfiguration and add #Configuration to the class, make sure your starter class has #EnableWebMvc
add this to MyRepositoryRestMvcConfiguration- extend TypeConstrainedMappingJackson2HttpMessageConverter as MyResourceSupportHttpMessageConverter
add this to MyRepositoryRestMvcConfiguration
#Override
#Bean
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() {
ArrayList<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaTypes.HAL_JSON);
if (config().useHalAsDefaultJsonMediaType()) {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 10
: Ordered.LOWEST_PRECEDENCE - 1;
TypeConstrainedMappingJackson2HttpMessageConverter converter = new MyResourceSupportHttpMessageConverter(
order);
converter.setObjectMapper(halObjectMapper());
converter.setSupportedMediaTypes(mediaTypes);
converter.getObjectMapper().addMixIn(Object.class, MyFilteringMixin.class);
final FilterProvider myRestrictionFilterProvider = new SimpleFilterProvider()
.addFilter("MyFilteringMixin", new MyPropertyFilter()).setFailOnUnknownId(false);
converter.getObjectMapper().setFilterProvider(myRestrictionFilterProvider);
return converter;
}
Create an empty Mixin
package filters;
import com.fasterxml.jackson.annotation.JsonFilter;
#JsonFilter("MyFilteringMixin")
public class MyFilteringMixin {}
Create an empty Mixin
create class MyPropertyFilter extending SimpleBeanPropertyFilter and override adapt this method
serializeAsField(Object, JsonGenerator, SerializerProvider, PropertyWriter)you need to call either super.serializeAsField(pPojo, pJgen, pProvider, pWriter) or pWriter.serializeAsOmittedField(pPojo, pJgen, pProvider) depending on whether you wish to include or discard this particular field.
I added an annotation to the particular fields I wanted to alter and interrogated that annotation when deciding which of these two to call. I injected the security role and stored permitted roles in the annotation.
This alters what Hal shares out to the caller, not what Hal is holding in its repository. Thus you can morph it depending on who the caller is.
I would like to configure and use a Spring 4.1 AsyncUncaughtExceptionHandler. According to the Spring team (see relevant comment here) one will be able to configure an AsyncUncaughtExceptionHandler either by with the <task:annotation-driven> or by implementing AsyncConfigurer as shown here:
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler() ;
}
Now my question is as follows: Is there another web-layer annotation similar to #ExceptionHandler that would work like a AsyncUncaughtExceptionHandler?
As stated in the comment, here's an approach I've taken:
It's about async data imports so all classes are called Import...
What I did not do (yet) is the uncaught exception handling, but reading your post made me think about it and it should be straight forward with Spring-AOP wrapping the Importer.process() methods. This will not be global solution but it would be adaptable for a complete application by using a more generalized Result object.
The Controller uses the ImportRequests to get processing (or done) messages. The Importer itself is not removing the results from the map but this is delegated to the controller instead (A user is clicking delete). We also have a #Scheduled task which cleans up done results after 1 hour to ensure there are not left-overs.
So here's part of the code that the Controller is able to get import results during processing:
#Service
public class ImportRequests {
private final Map<User, ImportResult> importRequests = new ConcurrentHashMap<>();
/** Add, remove, get methods for current user omitted */
}
public class ImportResult {
/** The done. */
private Future<Boolean> done;
/** The error messages. */
private List<String> messages = Collections.synchronizedList(new ArrayList<String>());;
}
#Service
public class ImportService {
#Autowired
private ImportRequests importRequests;
#Autowired
private Importer importer;
public ImportResult doImport(final ImportForm importForm) {
ImportResult result = new ImportResult();
importRequests.addImportResultForCurrentUser(result);
/* This is the actual Async call (process) */
result.setDone(importer.process(result));
return result;
}
}
#Service
public class ImporterImpl implements Importer {
/**
* doProcess will import the *big* file and update the result object with the necessary messages
*/
#Async
public Future<Boolean> process(ImportResult result) {
Boolean done = doProcess(result);
return new AsyncResult<Boolean>(done);
}
}
Hope this helps.
Original Text:
One possibility that I have used is the "#ControllerAdvice" on a class scanned by the servletcontext.
You simply create a method with the exception as a parameter and annotate that method with "#ExceptionHandler". You can even have multiple handlers for specific exception types.
The result of these methods are again handled by the DispatcherServlet, so you can render a view the same way as with request mappings.
Here is my requirement in Java 6: I am using Eclipse JUNO.
Annotate a method with a custom annotation.
During compilation, raise warning message if a method is calling the
annotated method.
I am looking for something like #Deprecated annotation.
This is what I have done:
Wrote a custom annotation.
Wrote an annotation processor to read and process the methods with
the annotation.
Created a jar and added it in annotation processor path. My sample code (see below) raises the warning message in the annotated method. But it is not my requirement.
What I couldn’t do:
I could not get the calling methods. I want to raise the warning
message in those calling methods.
My sample code:
Custom annotation:
package tool.apichecks;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.SOURCE)
#Target({ ElementType.METHOD })
public #interface HighCostMethod {
String altMethod();
}
Annotation Processor:
package tool.apichecks;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
#SupportedAnnotationTypes({ "tool.apichecks.HighCostMethod" })
public class MethodProcessor extends AbstractProcessor {
private enum MethodType {
HIGH_COST(HighCostMethod.class.getName());
private String name;
private MethodType(String name) {
this.name = name;
}
private static MethodType getMethodType(String name) {
MethodType methodType = null;
for (MethodType methodType2 : MethodType.values()) {
if (methodType2.name.equals(name)) {
methodType = methodType2;
break;
}
}
return methodType;
}
}
private ProcessingEnvironment processingEnvironment;
#Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
this.processingEnvironment = processingEnvironment;
}
#Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnvironment) {
if (!roundEnvironment.processingOver()) {
for (TypeElement annotation : annotations) {
final Set<? extends Element> elements = roundEnvironment
.getElementsAnnotatedWith(annotation);
MethodType methodType = MethodType.getMethodType(annotation
.toString());
for (Element element : elements) {
switch (methodType) {
case HIGH_COST: {
processHighCostMethod(element);
break;
}
}
}
}
}
return true;
}
protected void processHighCostMethod(Element element) {
HighCostMethod highCostMethod = element
.getAnnotation(HighCostMethod.class);
/* TODO This warns the annotated method itself. I don't want this. I want to warn the methods that calls this method */
processingEnvironment
.getMessager()
.printMessage(
Kind.WARNING,
String.format(
"Do not use high cost method %s. Instead use %s method.",
element, highCostMethod.altMethod()), element);
}
}
Using an AnnotationProcessor will only work on the files containing the annotations or overriding methods, but not calling methods. Maybe there's a way around this, but then you will probably be limited by projects, because the processor only looks at one project at a time.
I guess you need to write an Eclipse plugin with a builder, that analyses code in all files and checks called methods for annotations.
That a lot more work than an annotation processor, but you also have more options. E.g. you could implement a quick fix for the error markers.