5

I was trying to get simple webapp working with Guice and JPA on Jetty, using the persistence and servlet guice extensions.

I have written this Service implementation class:

public class PersonServiceImpl implements PersonService {

private EntityManager em;

@Inject
public PersonServiceImpl(EntityManager em) {
    this.em = em;
}

@Override
@Transactional
public void savePerson(Person p) {
    em.persist(p);
}

@Override
public Person findPerson(long id) {
    return em.find(Person.class, id);
}

@Override
@Transactional
public void deletePerson(Person p) {
    em.remove(p);
}

}

And this is my servlet (annotated with @Singleton):

@Inject
PersonService personService;

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    String name = req.getParameter("name");
    String password = req.getParameter("password");
    String email = req.getParameter("email");
    int age = Integer.valueOf(req.getParameter("age"));


    Person p = new Person();
    p.setAge(age);
    p.setName(name);
    p.setEmail(email);
    p.setPassword(password.toCharArray());

    logger.info("saving person");

    personService.savePerson(p);
    logger.info("saved person");

    logger.info("extracting person");
    Person person = personService.findPerson(p.getId());
    resp.getWriter().print("Hello " + person.getName());
}

When I run this it works, and I get the name sent to the client, but when I look at the log I see that there is no DML generated for the insertion and selection from postgresql does not return any results, which means it wasn't really persisted.

I debugged through the code and I saw that JpaLocalTxnInterceptor called txn.commit().

Then I made a change to PersonServiceImpl and used Provider<EntityManager> instead of just EntityManager and it worked as expected. Now I don't really understand why and it's probably because I don't really understand the idea behind Provider. On the Guice wiki page it says:

Note that if you make MyService a @Singleton, then you should inject Provider instead.

However, my PersonServiceImpl is not a @Singleton so I am not sure why it applies, perhaps it's because of the Servlet?

I would really appreciate if you could clear this out for me.

4

1 回答 1

13

You need Provider<EntityManager> because Guice's built-in persistence and servlet extensions expect EntityManager to be request-scoped. By injecting a request-scoped EntityManager from a service held in a singleton servlet, you're making a scope-widening injection, and Guice won't store data from a stale, mismatched EntityManager.

Providers

Provider is a one-method interface that exposes a get() method. If you inject a Provider<Foo> and then call get(), it will return an instance created the same way as if you had injected Foo directly. However, injecting the Provider allows you to control how many objects are created, and when they are created. This can be useful in a few cases:

  • only creating an instance if it's actually needed, especially if the creation takes lots of time or memory
  • creating two or more separate instances from within the same component
  • deferring creation to an initialization method or separate thread
  • mixing scopes, as described below

For binding of X, Provider<X>, or @Provides X, Guice will automatically allow you to inject either X or Provider<X> directly. You can use Providers without adjusting any of your bindings, and Providers work fine with binding annotations.

Scopes and scope-widening injections

Broadly speaking, scopes define the lifetime of the object. By default, Guice creates a new object for every injection; by marking an object @Singleton, you instruct Guice to inject the same instance for every injection. Guice's servlet extensions also support @RequestScoped and @SessionScoped injections, which cause the same object to be injected within one request (or session) consistently but for a new object to be injected for a different request (or session). Guice lets you define custom scopes as well, such as thread scope (one instance per thread, but the same instance across injections in the same thread).

@Singleton public class YourClass {
  @Inject HttpServletRequest request;  // BAD IDEA
}

What happens if you inject a request-scoped object directly from within a @Singleton component? When the singleton is created, it tries to inject the instance relevant to the current request. Note that there might not be a current request, but if there is one, the instance will be saved to a field in the singleton. As requests come and go, the singleton is never recreated, and the field is never reassigned--so after the very first request your component stops working properly.

Injecting a narrow-scope object (@RequestScoped) into a wide scope (@Singleton) is known as a scope-widening injection. Not all scope-widening injections show symptoms immediately, but all may introduce lingering bugs later.

How Providers help

PersonService isn't annotated with @Singleton, but because you're injecting and storing an instance in a @Singleton servlet, it might as well be a singleton itself. This means EntityManager also has singleton behavior, for the same reasons.

According to the page you quoted, EntityManager is meant to be short-lived, existing only for the session or request. This allows Guice to auto-commit the transaction when the session or request ends, but reusing the same EntityManager is likely preventing storage of data any time after the first. Switching to a Provider allows you to keep the scope narrow by creating a fresh EntityManager on every request.

(You could also make PersonService a Provider, which would also likely solve the problem, but I think it's better to observe Guice's best practices and keep EntityManager's scope explicitly narrow with a Provider.)

于 2015-03-01T10:22:46.910 回答