Wicket @SpringBean and Spring @Autowired with injection via constructor
Asked Answered
M

4

5

I have a Wicket panel in which I want to inject bean using @SpringBean

public class SomePanel extends Panel {

  @SpringBean
  private BlogSummaryMailGenerator blogSummaryMailGenerator;

}

But this BlogSummaryMailGenerator has injection via constructor defined like this:

@Component
public class BlogSummaryMailGenerator {

  private BlogRepository blogRepository;
  private BlogPostRepository blogPostRepository;

  @Autowired
  public BlogSummaryMailGenerator(BlogRepository blogRepository,
                                BlogPostRepository blogPostRepository) {
    this.blogRepository = blogRepository;
    this.blogPostRepository = blogPostRepository;
  }
}

And when SomePanel is instantiated I am getting an exception

Caused by: java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
at net.sf.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:721) ~[cglib-3.1.jar:na]
at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:499) ~[cglib-3.1.jar:na]
at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) ~[cglib-3.1.jar:na]
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216) ~[cglib-3.1.jar:na]
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) ~[cglib-3.1.jar:na]
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) ~[cglib-3.1.jar:na]
at org.apache.wicket.proxy.LazyInitProxyFactory.createProxy(LazyInitProxyFactory.java:191) ~[wicket-ioc-7.2.0.jar:7.2.0]

Adding empty no-args constructor to the BlogSummaryMailGenerator solves this issue but adding such code only to make injection work is wrong and I would like to avoid it.

Any suggestions how to make @SpringBean work with beans using injection via constructor?

Magma answered 7/2, 2016 at 23:42 Comment(1)
Do you use Spring configuration? If you do is it Java-based config or XML?Mattson
M
6

Solution

To be able to take advantage of constructor injection for @SpringBean in Wicket components you just have to add Objenesis to your compile time dependencies.

Explanation

Objenesis is a little and less known byte code manipulation library which (in opposite to CGLIB provided together with Wicket) is able to create a proxy object for a class which has no default (no args) constructor. If you add it as a compile dependency to your project then Wicket will switch it's internal lazily initializable proxy creation logic to take advantage of Objenesis (instead CGLIB which requires no args constructor) while instantiating a proxy. Unfortunately this feature is not documented but I can confirm it works in my case.

Manara answered 31/3, 2016 at 5:24 Comment(2)
You can find why adding Objenesis to the project classpath solves the problem by inspecting LazyInitProxyFactory source of the Apache Wicket project on GitHub.Damselfly
Hello @Marcin, I need your help too in fixing the java.lang.NoClassDefFoundError: Could not initialize class net.sf.cglib.proxy.Enhancer. I added Objenesis and the variable is returning true as well, however && !hasNoArgConstructor(type) is returning false because of which cglib is used again for type SavedRequestAwareAuthenticationSuccessHandler. Question link - https://mcmap.net/q/1176750/-apache-wicket-java-lang-noclassdeffounderror-could-not-initialize-class-net-sf-cglib-proxy-enhancer/819866 chat link - chat.stackoverflow.com/transcript/message/52009909#52009909Halda
H
8

The real problem is in CGLIB. It requires a default constructor to be able to create the proxy instance. The real Spring bean is created separately by Spring and has no such restrictions. The default constructor needed by CGLIB could be even private as far as I remember.

Update: Since Wicket 9.5.0 Wicket could also use ByteBuddy instead of CGLib.

Another solution is to use an interface for this bean. Then Wicket will use JDK Proxy instead of CGLIB and in this case there is no need of default constructor in the implementation.

Helles answered 8/2, 2016 at 8:14 Comment(0)
M
6

Solution

To be able to take advantage of constructor injection for @SpringBean in Wicket components you just have to add Objenesis to your compile time dependencies.

Explanation

Objenesis is a little and less known byte code manipulation library which (in opposite to CGLIB provided together with Wicket) is able to create a proxy object for a class which has no default (no args) constructor. If you add it as a compile dependency to your project then Wicket will switch it's internal lazily initializable proxy creation logic to take advantage of Objenesis (instead CGLIB which requires no args constructor) while instantiating a proxy. Unfortunately this feature is not documented but I can confirm it works in my case.

Manara answered 31/3, 2016 at 5:24 Comment(2)
You can find why adding Objenesis to the project classpath solves the problem by inspecting LazyInitProxyFactory source of the Apache Wicket project on GitHub.Damselfly
Hello @Marcin, I need your help too in fixing the java.lang.NoClassDefFoundError: Could not initialize class net.sf.cglib.proxy.Enhancer. I added Objenesis and the variable is returning true as well, however && !hasNoArgConstructor(type) is returning false because of which cglib is used again for type SavedRequestAwareAuthenticationSuccessHandler. Question link - https://mcmap.net/q/1176750/-apache-wicket-java-lang-noclassdeffounderror-could-not-initialize-class-net-sf-cglib-proxy-enhancer/819866 chat link - chat.stackoverflow.com/transcript/message/52009909#52009909Halda
M
0

The error message is pretty clear. Wicked trying to create instance of proxy class for BlogSummaryMailGenerator with CGLIB library. Since you didn't (or you can't) provide arguments to constructor, it looking for contstructor with no arguments. But it can't, and you get error.

Just replace constructor injection with property injection, and create no argument constructor:

@Component
public class BlogSummaryMailGenerator {

  @Autowired
  private BlogRepository blogRepository;

  @Autowired
  private BlogPostRepository blogPostRepository;

  public BlogSummaryMailGenerator() {}

}

Actually, you do not need to declare an empty constructor. I did it just for clarity. Note, that BlogRepository and BlogPostRepository should be declared as beans (marked with @Component annotation, or created as @Bean in Spring configuration).

UPDATE:

When you add SpringComponentInjector in your WebApplication.init(), you can specify false for third paramter, which means 'wrapInProxies'. Wicket will never wrap Spring beans in porxy, and you can use @Autowired for constructors.

@Override
public void init()
{

    super.init();

    AnnotationConfigApplicationContext springContext = 
         new AnnotationConfigApplicationContext();
    springContext.register(SpringConfig.class);
    springContext.refresh();
    getComponentInstantiationListeners().add(new SpringComponentInjector(this, 
         springContext, false));
}
Mattson answered 8/2, 2016 at 4:8 Comment(6)
But property injection is bad for testability and I want to avoid using such hacks just to use more sophisticated beans with Wicket. I hope that maybe there's a solution to use contructor based injection somehow.Magma
You have to understand, that this is not hack at all. In Spring world this way is one of three usual ways to inject data to bean.Mattson
Yes, but changing my approach to DI to worse one only to use SpringBean is not a good solution in my opinion. That is why I am looking for something better. But as @Helles points, it can be hard to overcome.Magma
Turning of wrapInProxies will have severe consequences especially in clustered environments. It will force Wicket components to be serialized along with its dependency graph which in best case scenario results in excessive memory usage and in worse case will broke the app due to serialization exceptions at runtime.Damselfly
@KenBekov I can agree that field injection is one of three ways to inject data to bean but it's error prone and not recommended appraoch even by Spring developers, see "Why field injection is evil" by Oliver Gierke, one of the Spring developers.Damselfly
@MarcinKłopotek thank you for link. It was useful to know this point of view.Mattson
C
0

The correct way to solve this is not to add Objenesis to your project, but to inject interfaces instead of concrete implementations, as @martin-g already explained (of course, we do not always have the privilege to be able to do the right thing, but when we do, we should do it).

I have a project that started to give the exact same error and stack after a library update I still don't exactly understand (complete Maven dependency hell, I inherited it, go easy on me). The reason was that I was creating a Spring request-scoped bean from a concrete subclass of ListModel<MyClass> and Wicket was hell bent on wrapping that class into a lazy loaded proxy, which it couldn't do because there was no zero-args-constructor.

I fixed it by changing the configuration class to create a named instance of IModel<List<MyClass>> and by defining the injected dependency using the name.

In the configuration class:

@Bean(name = "namedModel")
@RequestScope
public IModel<List<MyClass>> myObjectList() {
    return new MyClass(parameters);
}

In the component:

@Inject
@Named("namedModel")
private IModel<List<MyClass>> myModel;
Collis answered 5/10, 2021 at 11:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.