如何在应用程序启动时自动注册@Entity修饰类?

时间:2014-08-12 18:50:30

标签: java objectify google-app-engine

如何使Objectify在运行时自动发现带注释的Entity类?

我想在应用程序启动时注册我的类,而不必按照文档中的建议将它们硬编码到类中。

2 个答案:

答案 0 :(得分:2)

解决方案

这不是runtime反射成本解决方案,因为它会在compile/build/package时进行所有反射。

import com.google.appengine.api.ThreadManager;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.annotation.Entity;
import org.reflections.Reflections;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * This class processes the classpath for classes with the @Entity or @Subclass annotations from Objectify
 * and registers them with the ObjectifyFactory, it is multi-threaded and works very fast!
 */
public class ObjectifyLoaderContextListener implements ServletContextListener
{
    private static final Logger L = LoggerFactory.getLogger(ObjectifyLoaderContextListener.class);

    private final Set<Class<?>> entities;

    public ObjectifyLoaderContextListener()
    {
        this.entities = new HashSet<>();
    }

    @Override
    public void contextInitialized(@Nonnull final ServletContextEvent sce)
    {
        final ExecutorService es = Executors.newCachedThreadPool(ThreadManager.currentRequestThreadFactory());
        cb.setExecutorService(es);
        final Reflections r = Reflections.collect();
        this.entities.addAll(r.getTypesAnnotatedWith(Entity.class));
        es.shutdown();
        final ObjectifyFactory of = ObjectifyService.factory();
        for (final Class<?> cls : this.entities)
        {
            of.register(cls);
            L.debug("Registered {} with Objectify", cls.getName());
        }
    }

    @Override
    public void contextDestroyed(@Nonnull final ServletContextEvent sce)
    {
        /* this is intentionally empty */
    }
}

此课程的更新将在this gist中提供。

答案 1 :(得分:-1)

这是基于公认答案的另一种解决方案。

<强>摘要
如果要从服务中消除所有生产运行时成本(即使是拾取随后由#collect API使用的XML工件的相对较小的成本),您也可以将成本移至单元​​测试阶段,同时仍保留100%正在注册的所有实体的安全性。请参阅答案的结尾处。

动机&amp;详情
这有几个好处,如下所述,但它们是否值得,取决于您的用例。就我个人而言,这些问题是否会成为一个问题并非如此 - 考虑到避免在生产过程中做任何不必要的事情(以及从生产路径中删除代码),我不会这样做,我总是会接受它,只要它不会带来额外的复杂性,奇怪的设计成本等等。在这种情况下,没有。这是一种将运行时注册保证从prod转移到测试的教科书方法。

请注意,GAE实例可能会被任意重启多次。其他考虑因素包括您的应用程序对服务启动时间的敏感程度,生成的Reflection XML工件的大小(您的jar依赖项的函数和反映第三方库的详细信息)以及库如何有效地解析它们。虽然您可以通过多线程降低性能风险,但Google仍会向您收取使用的CPU时间费用。

将此转换为单元测试解决方案的相对好处是:

  1. 您每次重新启动服务时都可以消除解析XML资源并将其重建为Reflection元数据的成本(可能会多次任意发生)

  2. 您可以消除在未来某些时候更改项目JAR依赖关系的风险(对于非平凡的项目,依赖关系往往会随着时间的推移而增长),并且忘记重新评估XML资源的新大小及其解析;这可能会逐渐为您的服务重启时间增加更大的开销,这很难确定,但会影响用户(导致经典死亡人数减少)

  3. 您消除了对第三个库的运行时prod依赖性,这总是很好(依赖性将保留给您的单元测试模块)。例如,如果库的未来版本突然降低了XML解析速度,那么您需要在prod中处理它。这是一个可以消除的风险。

  4. 这些考虑因素是否仍然纯粹是学术性的,取决于您的使用案例和风险/成本容忍度。

    警告:如果由于某种原因您的发布推出过程没有强制要求通过单元测试,这显然不是正确的方法。但我不认为对很多人来说就是这种情况。

    <强>代码
    为了便于阅读,我省略了外部测试类(假设所有代码都存在于Test类中)

    @Test
    public void testObjectifyRegistration() {
        final ObjectifyFactory ofy = Ofy.factory(); // your backend-specific ofy entry point  where you've done your registrations
        for (final Class<?> cls : allEntities()) {
            verifyRegistrationState(ofy, cls, /*expected to be registered?*/!cls.equals(UnregisteredUnittestEntity.class));
        }
    }
    
    private Iterable<Class<?>> allEntities() {
        final ExecutorService es = Executors.newCachedThreadPool();
        try {
            final ConfigurationBuilder cb = new ConfigurationBuilder();
            cb.setUrls(ClasspathHelper.forPackage(""));
            cb.setExecutorService(es);
            return new HashSet<>(new Reflections(cb).getTypesAnnotatedWith(Entity.class));
        } finally {
            es.shutdown();
        }
    }
    
    private void verifyRegistrationState(final ObjectifyFactory ofy, final Class<?> cls, final boolean shouldBeRegistered) {
        RuntimeException missingRegistrationExc = null;
        try {
            ofy.getMetadata(cls); // per today's API contract, this method throws if cls has not been registered with objectify
        } catch (RuntimeException exc) {
            missingRegistrationExc = exc;
        }
        if (shouldBeRegistered && missingRegistrationExc != null)
            throw missingRegistrationExc;
        else if (!shouldBeRegistered && missingRegistrationExc == null)
            fail("ObjectifyFactory#getMetadata expected to throw upon unregistered entity, check for new contract of method and update test");
    }
    
    @Entity // self-test: used to validate correctness of the unit test itself
    private static class UnregisteredUnittestEntity {
        @VisibleForTesting
        public UnregisteredUnittestEntity() {
        }
    }
    

    以下是使用@Entity注释的类尚未在Objectify注册时的示例失败。

      

    java.lang.IllegalArgumentException:No class&#39; [xxx] .UserImpl&#39;是   已注册   com.googlecode.objectify.impl.Registrar.getMetadataSafe(Registrar.java:120)     在   com.googlecode.objectify.ObjectifyFactory.getMetadata(ObjectifyFactory.java:210)     在   [XXX] .OfyRegistrationTest.verifyRegistrationState(OfyRegistrationTest.java:56)     在   [XXX] .OfyRegistrationTest.testOfyRegistration(OfyRegistrationTest.java:37)