0
votes

I would like to use mybatis with container-managed transaction in my aplication. I use mybatis 3.4.2 and mybatis-cdi 1.0.0.

My code works but at the moment I open and close sessions manually and I do not know how to inject either SqlSession or Mapper to my EJB.

It seems that mybatis-cdi does not do its job properly in my case.

That is my deployment structure:

EAR
  +--- commons.jar (interfaces, POJOs)
  +--- ejb.jar (stateless EJBs + MyBatis mapper + session factory)
  +--- web.war (demo servlet which calls EJB)

commons.jar

/a/
/a/b/
/a/b/commons/
/a/b/commons/mybatis/
/a/b/commons/mybatis/SessionFactoryProducer.class
/a/b/commons/api/
/a/b/commons/api/EchoService.class
/a/b/commons/domain/
/a/b/commons/domain/Configuration.class
/META-INF/
/META-INF/beans.xml

SessionFactoryProducer.java (simple interface to produce SqlSessionFactory)

public interface SessionFactoryProducer {
    SqlSessionFactory produce() throws IOException;
}

EchoService.java (EJB interface)

public interface EchoService {
    String echo(String str) throws IOException;
}

Configuration.java (simple POJO)

class with getters/setters

beans.xml

<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">

    <interceptors>
        <class>org.mybatis.cdi.JtaTransactionInterceptor</class>
    </interceptors>

</beans>

ejb.jar

/a/
/a/b/
/a/b/ejb/
/a/b/ejb/EchoServiceBean.class
/a/b/ejb/dao/
/a/b/ejb/dao/ConfigurationDao.class
/a/b/ejb/SessionFactoryProducerImpl.class
/META-INF/
/META-INF/beans.xml

EchoServiceBean.java (simple stateless EJB)

@Stateless
public class EchoServiceBean implements EchoService {

    //@Inject
    //private SessionFactoryProducer sqlSessionFactoryProducer;

    //@Inject
    //private SqlSession sqlSession;

    @Inject
    private ConfigurationDao configurationDao;

    @Override
    public String echo(String str) throws IOException {
        // SqlSession sqlSession = sqlSessionFactoryProducer.produce().openSession();
        // ConfigurationDao configurationDao = sqlSession.getMapper(ConfigurationDao.class);
        Configuration configuration = configurationDao.findByKey("something");
        LOGGER.info(configuration.toString());

        sqlSession.close();
        return new Date() + ": Hello";
    }
}

ConfigurationDao.java (simple MyBatis mapper, nothing special here)

@Mapper
public interface ConfigurationDao {
    @Select("SELECT id, key_name, key_value, description "
            + "FROM application.configuration "
            + "WHERE key_name = #{key}")
    Configuration findByKey(@Param("key") String key);
}

SessionFactoryProducerImpl.java (EJB, produces MyBatis SqlSessionFactory):

@Stateless
public class SessionFactoryProducerImpl implements SessionFactoryProducer {
    @Override
    public SqlSessionFactory produce() throws IOException {
        LOGGER.info("MyBatis SessionFactory is initializing...");

        try (Reader reader = Resources.getResourceAsReader("mybatis.xml")) {
            SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
            LOGGER.info("Session factory has been obtained");
            return sessionFactory;
        }
    }
}

beans.xml

same then before

When I use my SessionFactoryProducerImpl EJB to get MyBatis session then everything works fine, but I would like to let manage SqlSession by EE container (open/close/commit/rollback).

The issue appeared after I modified my SessionFactoryProducerImpl as per suggestion of the official doc http://www.mybatis.org/cdi/injection.html (remove @Stateless and interface reference, add @Produces, @ApplicationScoped, @SessionFactoryProvider annotations) and after that

  1. I inject org.apache.ibatis.session.SqlSession instead of my SessionFactoryProducerImpl I get an Unsatisfied dependencies for type SqlSession with qualifiers @Default during deploy EAR to server
  2. I inject MyBatis mapper which is the ConfigurationDao in my case then I get a There are no SqlSessionFactory producers properly configured error.

What is the proper way to let EE container to manage MyBatis session?


UPDATE-1

I tried to inject SqlSessionFactory by name:

//@Stateless
public class SessionFactoryProducerImpl /*implements SessionFactoryProducer*/ {
    //@Override
    @ApplicationScoped
    @Produces
    @Named("fooManager")
    @SessionFactoryProvider
    public SqlSessionFactory produce() throws IOException {
       ...
    }
}

Usage:

@Stateless
public class EchoServiceBean implements EchoService {
    @Inject @Named("fooManager") ConfigurationDao configurationDao;
    ...
}

Log from app server:

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172749] [levelValue: 800] [[
  MyBatis CDI Module - Found class with @Mapper-annotation: ConfigurationDao]]

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172943] [levelValue: 800] [[
  MyBatis CDI Module - SqlSessionFactory producer SessionFactoryProducerImpl.produce]]

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172982] [levelValue: 800] [[
  MyBatis CDI Module - Activated]]

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172983] [levelValue: 800] [[
  MyBatis CDI Module - Found a bean, which needs a Mapper interface a.b.ejb.dao.ConfigurationDao]]

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172984] [levelValue: 800] [[
  MyBatis CDI Module - Managed Mapper dependency: a.b.ejb.dao.ConfigurationDao_fooManager, a.b.ejb.dao.ConfigurationDao]]

[Payara 4.1] [INFO] [org.mybatis.cdi.MybatisExtension] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972172984] [levelValue: 800] [[
  MyBatis CDI Module - Managed SqlSession: org.apache.ibatis.session.SqlSession_fooManager, org.apache.ibatis.session.SqlSession]]

[Payara 4.1] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972173275] [levelValue: 800] [[
  Loading application [ear-packager-1.0#web-1.0.war] at [/web]]]

[Payara 4.1] [INFO] [javax.enterprise.system.core] [tid: _ThreadID=803 _ThreadName=admin-thread-pool::admin-listener(40)] [timeMillis: 1492972173334] [levelValue: 800] [[
  ear-packager-1.0 was successfully deployed in 1,280 milliseconds.]]

But I still get exception when I try to use it:

Caused by: org.mybatis.cdi.MybatisCdiConfigurationException: There are no SqlSessionFactory producers properly configured.
    at org.mybatis.cdi.CDIUtils.findSqlSessionFactory(CDIUtils.java:55)
    at org.mybatis.cdi.SerializableMapperProxy.getMapper(SerializableMapperProxy.java:57)
    at org.mybatis.cdi.SerializableMapperProxy.<init>(SerializableMapperProxy.java:44)
    at org.mybatis.cdi.MyBatisBean.create(MyBatisBean.java:116)
    at org.jboss.weld.context.unbound.DependentContextImpl.get(DependentContextImpl.java:70)
    at org.jboss.weld.bean.ContextualInstanceStrategy$DefaultContextualInstanceStrategy.get(ContextualInstanceStrategy.java:100)
    at org.jboss.weld.bean.ContextualInstance.get(ContextualInstance.java:50)
    at org.jboss.weld.manager.BeanManagerImpl.getReference(BeanManagerImpl.java:744)
    at org.jboss.weld.manager.BeanManagerImpl.getInjectableReference(BeanManagerImpl.java:844)
    at org.jboss.weld.injection.FieldInjectionPoint.inject(FieldInjectionPoint.java:92)
    at org.jboss.weld.util.Beans.injectBoundFields(Beans.java:362)
    at org.jboss.weld.util.Beans.injectFieldsAndInitializers(Beans.java:373)
    at org.jboss.weld.injection.producer.DefaultInjector$1.proceed(DefaultInjector.java:71)
    at org.glassfish.weld.services.InjectionServicesImpl.aroundInject(InjectionServicesImpl.java:173)
    at org.jboss.weld.injection.InjectionContextImpl.run(InjectionContextImpl.java:46)
    at org.jboss.weld.injection.producer.DefaultInjector.inject(DefaultInjector.java:73)
    at org.jboss.weld.injection.producer.StatelessSessionBeanInjector.inject(StatelessSessionBeanInjector.java:60)
    at org.jboss.weld.injection.producer.ejb.SessionBeanInjectionTarget.inject(SessionBeanInjectionTarget.java:140)
    at org.glassfish.weld.services.JCDIServiceImpl.injectEJBInstance(JCDIServiceImpl.java:261)
    at com.sun.ejb.containers.BaseContainer.injectEjbInstance(BaseContainer.java:1698)
    at com.sun.ejb.containers.StatelessSessionContainer.createStatelessEJB(StatelessSessionContainer.java:488)
    ... 50 more

Any idea what is wrong in my code?


UPDATE-2

Interesting thing. I have just added a new servlet to the war project to display the list of the available beans in CDI container:

@WebServlet("/cdi")
public class CdiServlet extends HttpServlet {

    @Inject
    BeanManager beanManager;

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

        Set<Bean<?>> beans = beanManager.getBeans(Object.class, new AnnotationLiteral<Any>() {});

        ...
    }
}

I can see that there are 74 beans in my EE container and the most important beans are:

(4)
toString(): Extension [class org.mybatis.cdi.MybatisExtension] with qualifiers [@Default]; jar:file:/home/soma/applications/servers/_gombi_/payara-middleware/glassfish/domains/domain1/applications/ear-packager-1.0/lib/mybatis-cdi-1.0.0.jar!/META-INF/services/javax.enterprise.inject.spi.Extension@1[org.mybatis.cdi.MybatisExtension@643ecfa7]
getName(): org.mybatis.cdi.MybatisExtension
getSimpleName(): MybatisExtension
getSuperclass(): class java.lang.Object
getPackage(): org.mybatis.cdi
getAnnotations(): []


(6)
toString(): Managed Bean [class org.mybatis.cdi.CDIUtils$SerializableAnyAnnotationLiteral] with qualifiers [@Any @Default]
getName(): org.mybatis.cdi.CDIUtils$SerializableAnyAnnotationLiteral
getSimpleName(): SerializableAnyAnnotationLiteral
getSuperclass(): class javax.enterprise.util.AnnotationLiteral
getPackage(): org.mybatis.cdi
getAnnotations(): []

(8)
toString(): Extension [class org.glassfish.cdi.transaction.TransactionalExtension] with qualifiers [@Default]; bundle://302.0:0/META-INF/services/javax.enterprise.inject.spi.Extension@1[org.glassfish.cdi.transaction.TransactionalExtension@665874cc]
getName(): org.glassfish.cdi.transaction.TransactionalExtension
getSimpleName(): TransactionalExtension
getSuperclass(): class java.lang.Object
getPackage(): org.glassfish.cdi.transaction
getAnnotations(): []

(18)
toString(): Managed Bean [class org.mybatis.cdi.SqlSessionManagerRegistry] with qualifiers [@Any @Default]
getName(): org.mybatis.cdi.SqlSessionManagerRegistry
getSimpleName(): SqlSessionManagerRegistry
getSuperclass(): class java.lang.Object
getPackage(): org.mybatis.cdi
getAnnotations(): [@javax.enterprise.context.ApplicationScoped()]

(35)
toString(): Managed Bean [class org.mybatis.cdi.CDIUtils] with qualifiers [@Any @Default]
getName(): org.mybatis.cdi.CDIUtils
getSimpleName(): CDIUtils
getSuperclass(): class java.lang.Object
getPackage(): org.mybatis.cdi
getAnnotations(): []

(46)
toString(): Managed Bean [class a.b.commons.domain.Configuration] with qualifiers [@Any @Default]
getName(): a.b.commons.domain.Configuration
getSimpleName(): Configuration
getSuperclass(): class java.lang.Object
getPackage(): a.b.commons.domain
getAnnotations(): []

(48)
toString(): Session bean [class a.b.ejb.EchoServiceBean with qualifiers [@Any @Default]; local interfaces are [EchoService]
getName(): a.b.ejb.EchoServiceBean
getSimpleName(): EchoServiceBean
getSuperclass(): class java.lang.Object
getPackage(): a.b.ejb
getAnnotations(): [@javax.ejb.Stateless(name=, description=, mappedName=)]

(55)
toString(): Managed Bean [class org.mybatis.cdi.CDIUtils$SerializableDefaultAnnotationLiteral] with qualifiers [@Any @Default]
getName(): org.mybatis.cdi.CDIUtils$SerializableDefaultAnnotationLiteral
getSimpleName(): SerializableDefaultAnnotationLiteral
getSuperclass(): class javax.enterprise.util.AnnotationLiteral
getPackage(): org.mybatis.cdi
getAnnotations(): []


(56)
toString(): Managed Bean [class a.b.web.CdiServlet] with qualifiers [@Any @Default]
getName(): a.b.web.CdiServlet
getSimpleName(): CdiServlet
getSuperclass(): class javax.servlet.http.HttpServlet
getPackage(): a.b.web
getAnnotations(): [@javax.servlet.annotation.WebServlet(loadOnStartup=-1, initParams=[], urlPatterns=[], displayName=, largeIcon=, name=, asyncSupported=false, description=, smallIcon=, value=[/cdi])]

(58)
toString(): Producer Method [SqlSessionFactory] with qualifiers [@Any @Default] declared as [[BackedAnnotatedMethod] @ApplicationScoped @Produces @SessionFactoryProvider public a.b.ejb.SessionFactoryProducerImpl.produce()]
getName(): a.b.ejb.SessionFactoryProducerImpl
getSimpleName(): SessionFactoryProducerImpl
getSuperclass(): class java.lang.Object
getPackage(): a.b.ejb
getAnnotations(): []

(70)
toString(): Managed Bean [class a.b.ejb.SessionFactoryProducerImpl] with qualifiers [@Any @Default]
getName(): a.b.ejb.SessionFactoryProducerImpl
getSimpleName(): SessionFactoryProducerImpl
getSuperclass(): class java.lang.Object
getPackage(): a.b.ejb
getAnnotations(): []

72)
toString(): Managed Bean [class a.b.commons.domain.Configuration] with qualifiers [@Any @Default]
getName(): a.b.commons.domain.Configuration
getSimpleName(): Configuration
getSuperclass(): class java.lang.Object
getPackage(): a.b.commons.domain
getAnnotations(): []

I can see that my SessionFactoryProducerImpl is injected either with or without @Stateless annotation (bean id 70). I also can see that SqlSessionFactory producer is injected as well, bean id 58.

But I still get org.mybatis.cdi.MybatisCdiConfigurationException: There are no SqlSessionFactory producers properly configured error when I call the echo(...) method of my EJB.

I guess that somehow MyBatis needs to use producer method from a.b.ejb.SessionFactoryProducerImpl. But how to tell it to mybatis-cdi?

1
First of all try to deploy the example App. It has been tested with many Application Servers and it has a sample EJB. Then just change mybatis-config_1.xml to use MANAGED transactions. The Session Factory Producer is not intended to be used directly but internally by mybatis-cdi. Link to example app: github.com/mnesarco/mybatis-cdi-samplesmnesarco

1 Answers

0
votes

The documentation does not tell to remove @Stateless, it just does not specify it is required because it describes general use case. Then try:

@Stateless
@Local(SessionFactoryProducer.class)
public class SessionFactoryProducerImpl implements SessionFactoryProducer {
    //@Override
    @ApplicationScoped
    @Produces
    @Named("fooManager")
    @SessionFactoryProvider
    public SqlSessionFactory produce() throws IOException {
       ...
    }
}

@Local(SessionFactoryProducer.class) and implements SessionFactoryProducer might even be not mandatory.

I suppose @Stateless turn the SessionFactoryProducer into an EJB that will then be available in the same context/scope and can be injected into another.

The SessionFactoryProduceris just required to be present. Afterwards this is CDI's job to invoke it when it finds SqlSession or Mapper injection points.

This is pretty much what I have done, the session is managed by EJB. Of course provided mybatis-config.xml:

<environments default="development">
  <environment id="development">
    <transactionManager type="MANAGED">
    <dataSource type="JNDI">