Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.1.1] Parent class methods are not registered as controller actions #9731

Closed
droggo opened this issue Feb 24, 2016 · 18 comments
Closed

[3.1.1] Parent class methods are not registered as controller actions #9731

droggo opened this issue Feb 24, 2016 · 18 comments
Assignees
Milestone

Comments

@droggo
Copy link
Contributor

droggo commented Feb 24, 2016

After migration from 2.5.2 to 3.1.1, methods defined in abstract parent class are not recognized as controller actions. It doesn't matter if file is defined in grails-app/controllers or src/main.

It may be worked around using @grails.web.Action annotation on methods which should be actions. But this will not work if action has any parameters defined.

So:

abstract class ParentController {
    def a(){ render 'a' }
}
class SpecificController extends ParentController {
}

Will return 404 on specific/a request.

abstract class ParentController {
    @Action
    def a(){ render 'a' }
}
class SpecificController extends ParentController {
}

Will render a as request response - which is fine.

abstract class ParentAppController {
    @Action
    def b(Long id){ render 'b' }
}
class SpecificAppController extends ParentAppController {
}

Will throw exception during application start:

ERROR org.springframework.boot.SpringApplication - Application startup failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'grailsApplicationPostProcessor' defined in abstractparentcontroller.Application: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [grails.boot.config.GrailsApplicationPostProcessor]: Factory method 'grailsApplicationPostProcessor' threw exception; nested exception is org.grails.core.exceptions.GrailsRuntimeException: Error instantiated artefact class [class abstractparentcontroller.SpecificAppController] of type [class org.grails.core.DefaultGrailsControllerClass]: InvocationTargetException
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:599) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1123) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1018) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:120) ~[spring-context-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:678) ~[spring-context-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:520) ~[spring-context-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118) ~[spring-boot-1.3.2.RELEASE.jar:1.3.2.RELEASE]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766) ~[spring-boot-1.3.2.RELEASE.jar:1.3.2.RELEASE]
        at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361) ~[spring-boot-1.3.2.RELEASE.jar:1.3.2.RELEASE]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) ~[spring-boot-1.3.2.RELEASE.jar:1.3.2.RELEASE]
        at grails.boot.GrailsApp.run(GrailsApp.groovy:55) [grails-core-3.1.1.jar:3.1.1]
        at grails.boot.GrailsApp.run(GrailsApp.groovy:347) [grails-core-3.1.1.jar:3.1.1]
        at grails.boot.GrailsApp.run(GrailsApp.groovy:336) [grails-core-3.1.1.jar:3.1.1]
        at grails.boot.GrailsApp$run.call(Unknown Source) [grails-core-3.1.1.jar:3.1.1]
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) [groovy-2.4.5.jar:2.4.5]
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113) [groovy-2.4.5.jar:2.4.5]
        at org.codehaus.groovy.runtime.callsite.AbstractCacall(AbstractCallSite.java:133) [groovy-2.4.5.jar:2.4.5]
        at abstractparentcontroller.Application.main(Application.groovy:8) [main/:na]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [grails.boot.config.GrailsApplicationPostProcessor]: Factory method 'grailsApplicationPostProcessor' threw exception; nested exception is org.grails.core.exceptions.GrailsRuntimeException: Error instantiated artefact class [class abstractparentcontroller.SpecificAppController] of type [class org.grails.core.DefaultGrailsControllerClass]: InvocationTargetException
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        ... 23 common frames omitted
Caused by: org.grails.core.exceptions.GrailsRuntimeException: Error instantiated artefact class [class abstractparentcontroller.SpecificAppController] of type [class org.grails.core.DefaultGrailsControllerClass]: InvocationTargetException
        at grails.core.ArtefactHandlerAdapter.newArtefactClass(ArtefactHandlerAdapter.java:170) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.core.DefaultGrailsApplication.addArtefact(DefaultGrailsApplication.java:784) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.core.DefaultGrailsApplication.addArtefact(DefaultGrailsApplication.java:480) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.core.DefaultGrailsApplication.addArtefact(DefaultGrailsApplication.java:751) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.boot.config.GrailsApplicationPostProcessor.performGrailsInitializationSequence(GrailsApplicationPostProcessor.groovy:106) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.boot.config.GrailsApplicationPostProcessor.initializeGrailsApplication(GrailsApplicationPostProcessor.groovy:91) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.boot.config.GrailationPostProcessor.setApplicationContext(GrailsApplicationPostProcessor.groovy:209) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.boot.config.GrailsApplicationPostProcessor.<init>(GrailsApplicationPostProcessor.groovy:76) ~[grails-core-3.1.1.jar:3.1.1]
        at grails.boot.config.GrailsAutoConfiguration.grailsApplicationPostProcessor(GrailsAutoConfiguration.groovy:62) ~[grails-core-3.1.1.jar:3.1.1]
        at abstractparentcontroller.Application.grailsApplicationPostProcessor(Application.groovy) [main/:na]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_60]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_60]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_60]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_60]
        at org.springsource.loaded.ri.ReflectiveInterceptor.jlrMethodInvoke(ReflectiveInterceptor.java:1426) ~[springloaded-1.2.5.RELEASE.jar:1.2.5.RELEASE]
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
        ... 24 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_60]
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_60]
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_60]
        at java.lang.reflect.Constructor.newInstance(Constructor.java:422) ~[na:1.8.0_60]
        at org.springsource.loaded.ri.ReflectiveInterceptor.jlrConstructorNewInstance(ReflectiveInterceptor.java:1075) ~[springloaded-1.2.5.RELEASE.jar:1.2.5.RELEASE]
        at grails.core.ArtefactHandlerAdapter.newArtefactClass(ArtefactHandlerAdapter.java:161) ~[grails-core-3.1.1.jar:3.1.1]
        ... 39 common frames omitted
Caused by: org.grails.core.exceptions.GrailsConfigurationException: Cannot find invokable controller action: b
        at org.grails.core.DefaultGrailsControllerClass.methodStrategy(DefaultGrailsControllerClass.java:122) ~[grails-core-3.1.1.jar:3.1.1]
        at org.grails.core.DefaultGrailsControllerClass.<init>(DefaultGrailsControllerClass.java:67) ~[grails-core-3.1.1.jar:3.1.1]
        ... 45 common frames omitted
Caused by: java.lang.NoSuchMethodException: no such method: abstractparentcontroller.ParentAppController.b()Object/invokeVirtual
        at java.lang.invoke.MemberName.makeAccessException(MemberName.java:873) ~[na:1.8.0_60]
        at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:990) ~[na:1.8.0_60]
        at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:1382) ~[na:1.8.0_60]
        at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:858) ~[na:1.8.0_60]
        at org.grails.core.DefaultGrailsControllerClass.methodStrategy(DefaultGrailsControllerClass.java:119) ~[grails-core-3.1.1.jar:3.1.1]
        ... 46 common frames omitted
Caused by: java.lang.NoSuchMethodError: abstractparentcontroller.ParentAppController.b()Ljava/lang/Object;
        at java.lang.invoke.MethodHandleNatives.resolve(Native Method) ~[na:1.8.0_60]
        at java.lang.invoke.MemberName$Factory.resolve(MemberName.java:962) ~[na:1.8.0_60]
        at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:987) ~[na:1.8.0_60]
        ... 49 common frames omitted

It can be solved creating two methods in abstract class and manually calling desired method.

abstract class ParentAppController {
    @Action
    def b() {b(params.id)}
    def b(Long id){ render 'b' }
}
class SpecificAppController extends ParentAppController {
}

Two issues with this approach are:

  • developer has to rewrite what Grails is normally doing - handle params/JSON/other requests
  • if parameter is/are CommandObject or DomainObject, beans must be created and autowired in some way

Is this a bug?
Is there a way to create required action method without parameters using existing Grails classes?

@osscontributor osscontributor self-assigned this Feb 24, 2016
@osscontributor osscontributor added this to the grails-3.1.2 milestone Feb 24, 2016
@osscontributor
Copy link
Member

I think you can also work around it by making the parent class non-abstract.

We will take a look and get it straightened out. Thanks!

@droggo
Copy link
Contributor Author

droggo commented Feb 24, 2016

@jeffbrown I just did quick check. For parent class in src/main removing abstract doesn't change anything to any case described. For class in grails-app/controller it works but it also creates another controller endpoint which may be problematic unless we block it for example using UrlMappings

Thank you for the support

@osscontributor
Copy link
Member

For parent class in src/main removing abstract doesn't change anything to any case described. For class in grails-app/controller it works but it also creates another controller endpoint which may be problematic unless we block it for example using UrlMappings.

I wouldn't expect it to work under src/main but it should work under grails-app/controllers/.

The existence of a controller should not create any endpoints at all. I don't understand that. URL mappings result in endpoints.

@droggo
Copy link
Contributor Author

droggo commented Feb 24, 2016

This is what I meant. Thinking that default new application and probably most of existing ones will have some 'universal' url mapping with /$controller/.

@osscontributor
Copy link
Member

@droggo Is it your request that if a controller extends an abstract class that is defined under src/main/groovy/ then the methods in that parent class should be configured as controller actions but the parent class should not be configured as a controller? Just clarifying.

@droggo
Copy link
Contributor Author

droggo commented Feb 24, 2016

Yes, this is how it was working in Grails 2.

Because abstract class from grails-app/controllers is not configured as controller, this way is as good.

If this is not possible because of Grails 3 architecture, annotating methods with @Action is also completely fine (maybe even better? - it's more descriptive). In such case we would need support for methods with parameters though

@osscontributor
Copy link
Member

I don't think it is the case that it is not possible. We just need to agree on desired behavior and then we can make it so. :)

@jameskleeh
Copy link
Contributor

@droggo @jeffbrown I think having to mark the methods with @Action is better. I think the core issue is that it should support methods with parameters.

@droggo
Copy link
Contributor Author

droggo commented Feb 25, 2016

@Schlogen I have mixed feelings. From one point of view, you have to decide that this is action, and it's more secure, from other we break convention and go back to annotations...

@jeffbrown Maybe also traits should be considered? Here may be the issue that a lot of Core traits are used and those already add public methods which are not and should not be actions. But I've already seen few questions on Slack about adding controller methods with traits

@graemerocher
Copy link
Member

if you have a class in src/main/groovy there is no way for us to know that it is a controller class. So if you want it to be one you should annotate the class with grails.web.Controller

@droggo
Copy link
Contributor Author

droggo commented Feb 25, 2016

We don't want to add controllers from src/main/groovy, the point is to add common methods to existing controllers using super class, trait, or some other method - without copy/paste

@graemerocher
Copy link
Member

But that is not true, you're adding actions, that isn't a common method :)

@droggo
Copy link
Contributor Author

droggo commented Feb 25, 2016

Sorry for my english. By common I meant controller actions which will be the same in multiple controllers and to not copy/paste code and keep it DRY, super class could be used.

For example generated controllers containing show/update/save actions are nearly the same. We just created superclass which delivers all those methods. Controllers extending this class only defines entity which is handled.

@graemerocher
Copy link
Member

Sure, but that super class is a controller, and for Grails to treat it us such it needs to compile it as such. The only way it can know to compile the class as a controller is if you annotate it with grails.web.Controller

@droggo
Copy link
Contributor Author

droggo commented Feb 25, 2016

Here's the summary after a discussion on Slack - https://grails.slack.com/archives/questions/p1456394917001749

It is not supported by Grails 3 to add controller actions through inheritance (abstract class, trait, super class) unless class is marked with @controller or is a controllers from grails-app/controllers. Classes are made controllers at compile time, what brings this limitation. If super class is not a controller it does not have access to controller methods like render and developer will run into more issues than it's worth.

Working approach is to create standard controller in grails-app/controllers which then can be extended. If this controller should not be an endpoint, access to it can be blocked using UrlMappings

This is also related to abstract domain classes - grails/grails-data-mapping#639

@andreasschiestl
Copy link

Hi droggo

Sorry for digging out this already closed issue, I'm not even sure if anybody is notified about my comment.

I have tried to implement something similar like you, I want to abstract some of the methods that are very similar in most of the controller, like create/save/edit/update/delete.

I already achived it to inherit actions from a base controller class, but i'm not able to create or save any entity. Normally a action look something like this (simplified):

    def save(Address address) {
        // validation and error handling
        address.save(flush:true)
        // rendering view
    }

Now the method must be changed to something dynamic that is not fixed on a concrete domain class. I already tried to use a template class but it did not work :(

You wrote:

Controllers extending this class only defines entity which is handled.

How did you do that?

@snimavat
Copy link
Contributor

@anderskristian you use generics, look at RestFulController for example

@snimavat
Copy link
Contributor

snimavat commented Jul 24, 2016

So we have an abstract BaseCrudController which is in /src/main/groovy - it is very similar to RestFulController, and provides CRUD actions (create/edit/save/update/delete/list)

Putting the @ArteFact("Controller") annotation on the class BaseCrudController seem to solve the issue and the actions are available to subclasses.

So, BookController extends BaseCrudController and all CRUD actions are available to Book controller. So looks like the @ArteFact("Controller") is the solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants