Grails开发中,如何在多个项目中共享 GORM Domain classes?

参考资料
https://guides.grails.org/grails-multi-project-build/guide/index.html

我们只需要将需要共享的 Entity 放到一个 plugin 下面去就可以了,然后在多个 application 中使用该插件即可。

gradle 多项目结构

记得在 root project 中添加 settings.gradle 文件,并 include 子项目。

这里需要注意的是,整个目录结构中只能有一个 settings.gradle 文件,在根目录,否则子项目中的gradle任务执行会失败。

settings.gradle 文件示例

rootProject.name='chess_lottery'

include 'chess_plugin'
include 'chess_admin'
include 'chess_api'

创建多个项目后,我们需要清理一下 gradle 的文件,制作一个多项目 gradle 目录。

Move Gradle files to Root Project

multiproject$ mv app/gradlew .
multiproject$ mv app/gradlew.bat .
multiproject$ mv app/gradle.properties .
multiproject$ mv app/gradle .

删除掉其他项目下的上述文件,只保留根目录中的 gradle 相关文件,注意,grails 相关的文件不必要删除。

配置 web application 依赖 plugin 项目

在需要使用插件的 web application 项目的 build.gradle 中,添加如下依赖。

grails {
    plugins {
        // 对多租户安全插件的依赖
        compile project(":multi-tenant-security-plugin")
    }
    // 解决命令行过程问题
    pathingJar = true
}

完毕。

创建 plugin 的命令

grails create-plugin golden-case-lead-domain

去掉已经失效的 test 依赖

删除 testCompile "org.grails:grails-plugin-testing" 依赖,否则编译出错,这个依赖已经被废弃不用了。

修改插件默认包名

修改 grails-app/conf/application.yml 文件的 “defaultPackage” 属性。

---
grails:
    profile: web-plugin
    codegen:
        defaultPackage: com.telecwin.gcl.domain

这样,在插件里创建 domain class 等就有正确的包名了。

填写插件信息

编辑你的 ***GrailsPlugin 类,填写正确的属性,如 title, author, description 等。

grails 可以优化的地方

我用的是最新的 grails v4.0.3,存在下面的不做。

  1. 用 create-plugin 命令创建插件时,不能指定报名,会用插件名作为报名,通常这不是我们需要的,还需要手工修改一次。
  2. 有废弃的 test 依赖,需要清理掉。

等有时间了,应该给 grails 贡献一个 pull request。

如何将 Domain Classes 共享给其他的非 grails 项目?

用 plugin 共享的方式给非 grails 项目使用,会有一个问题,就是 plugin 依赖了大量 grails 相关的 jar,而这些其实是不需要的。怎么解决这个问题呢?也许可以将 domain class 放到一个 普通 groovy 项目中,让 grails app 使用这个 groovy lib 项目。但是如何告诉 grails 哪些是 domain class 呢?

domain-class-plugin of grails:

https://github.com/grails/grails-core/blob/master/grails-plugin-domain-class/src/main/groovy/org/grails/plugins/domain/DomainClassGrailsPlugin.groovy

我们可以尝试为 Domain Class 添加 @Entity 注解,这样项目只需要依赖 GORM。

尝试了一下,会报告下面的异常,找不到 domain class,但明明已经添加了项目依赖的:

java.lang.NoClassDefFoundError: com/telecwin/gcl/domain/LoginResult
	at com.telecwin.jinanyuan.BootStrap$_closure1$_closure4$_closure5.doCall(BootStrap.groovy:23)
	at com.telecwin.jinanyuan.BootStrap$_closure1$_closure4$_closure5.doCall(BootStrap.groovy)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:101)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
	at groovy.lang.Closure.call(Closure.java:405)
	at groovy.lang.Closure.call(Closure.java:399)
	at grails.util.Environment$EnvironmentBlockEvaluator.execute(Environment.java:554)
	at grails.util.Environment.executeForEnvironment(Environment.java:535)
	at grails.util.Environment.executeForCurrentEnvironment(Environment.java:510)
	at org.grails.web.servlet.boostrap.DefaultGrailsBootstrapClass.callInit(DefaultGrailsBootstrapClass.java:74)
	at org.grails.web.servlet.context.GrailsConfigUtils.executeGrailsBootstraps(GrailsConfigUtils.java:83)
	at org.grails.plugins.web.servlet.context.BootStrapClassRunner.onStartup(BootStrapClassRunner.groovy:56)
	at grails.boot.config.GrailsApplicationPostProcessor.onApplicationEvent(GrailsApplicationPostProcessor.groovy:269)
	at grails.boot.config.GrailsApplicationPostProcessor.onApplicationEvent(GrailsApplicationPostProcessor.groovy)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:402)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:359)
	at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:896)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:552)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
	at grails.boot.GrailsApp.run(GrailsApp.groovy:97)
	at grails.boot.GrailsApp.run(GrailsApp.groovy:458)
	at grails.boot.GrailsApp.run(GrailsApp.groovy:445)
	at com.telecwin.jinanyuan.Application.main(Application.groovy:11)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: java.lang.ClassNotFoundException: com.telecwin.gcl.domain.LoginResult
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at org.springframework.boot.devtools.restart.classloader.RestartClassLoader.loadClass(RestartClassLoader.java:144)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	... 41 common frames omitted

查看打包后的 war 中的确是有 domain-class jar 的,问题可能出在 grails 运行命令上。

grails 的 application.yml 文件不能写中文的注释,即 #中文 字样,否则用 grails run 启动会报错,可能是一个 bug。

打包成 war 启动后,domain jar 中的class能找到,但grails报告说 domain class 不是一个 GORM 没有正确配置,估计是 session 没有打开的原因:

2020-04-12 20:51:53.029 ERROR --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Either class [com.telecwin.gcl.domain.LoginResult] is not a domain class or GORM has not been initialized correctly or has already been shutdown. Ensure GORM is loaded and configured correctly before calling any methods on a GORM entity.
        at org.grails.datastore.gorm.GormEnhancer.stateException(GormEnhancer.groovy:467)
        at org.grails.datastore.gorm.GormEnhancer.findInstanceApi(GormEnhancer.groovy:315)
        at org.grails.datastore.gorm.GormEnhancer.findInstanceApi(GormEnhancer.groovy:312)
        at org.grails.datastore.gorm.GormEntity$Trait$Helper.currentGormInstanceApi(GormEntity.groovy:1366)
        at org.grails.datastore.gorm.GormEntity$Trait$Helper.save(GormEntity.groovy:100)
        at org.grails.datastore.gorm.GormEntity$Trait$Helper$save$1.call(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:127)
        at com.telecwin.gcl.domain.LoginResult.save(LoginResult.groovy)
        at com.telecwin.gcl.domain.LoginResult.save(LoginResult.groovy)
        at org.grails.datastore.gorm.GormEntity$save.call(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:119)
        at com.telecwin.jinanyuan.BootStrap$_closure1$_closure4$_closure5.doCall(BootStrap.groovy:24)
        at com.telecwin.jinanyuan.BootStrap$_closure1$_closure4$_closure5.doCall(BootStrap.groovy)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:101)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
        at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
        at groovy.lang.Closure.call(Closure.java:405)
        at groovy.lang.Closure.call(Closure.java:399)

不是 GORM session 的问题,是 grails 不支持简单添加 @Entity 就成为 domain class。需要使用其他手段。

ArtefactHandler 可以注册一个 domain class。

所有 grails 类都实现接口 GrailsClass 。domain class 由 DocumentPersistentEntity 代表。

org.grails.datastore.mapping.model.MappingContext#addPersistentEntity(java.lang.Class) 添加一个 class 为 domain class。

Bootstrap 类是如何被调用的?是否可以引用 grails 的 spring bean ?

被 grails.util.Environment.EnvironmentBlockEvaluator 调用
被 org.grails.web.servlet.context.GrailsConfigUtils#executeGrailsBootstraps(grails.core.GrailsApplication, org.springframework.web.context.WebApplicationContext, javax.servlet.ServletContext, grails.plugins.GrailsPluginManager) 调用

从 servletContext.setAttribute(GrailsApplication.APPLICATION_ID, application); 中可以取到 GrailsApplication 对象。

用 MappingContext 配置 hibernate 的工作是 HibernateMappingContextConfiguration 类做的。

setPackagesToScan

是这里配置的 persistent classes:

org.grails.orm.hibernate.connections.HibernateConnectionSourceFactory#buildConfiguration

HibernateConnectionSourceFactory(Class…classes) <— 这里设置了domain classes

当 GroovyClassLoader 装载一个 grails class 的时候,会根据 ClassInjector 接口以及子接口,例如 GrailsDomainClassInjector 来运行 AST Transformer,添加额外的属性、方法、注解等。

实际做 AST 的是 DefaultGrailsDomainClassInjector,这个类是在 编译的时候被调用,属于 AOT 变形。

判断一个 class 是不是 domain class 的方法:org.grails.compiler.injection.GrailsASTUtils#isDomainClass
这个方法会参考源代码所在路径,是否在 grails-app/domain 目录下。

因此成为一个可以被 AST Transform 的class可以是:

  • 在domain目录下
  • 或有 grails.persistence.Entity.class 注解
  • 或有 javax.persistence.Entity.class 注解

GrailsAutoConfiguration 是引导类。

设断点可以看到所有被装载的 domain class:
org.grails.orm.hibernate.connections.HibernateConnectionSourceFactory#HibernateConnectionSourceFactory

重载 GrailsAutoConfiguration 的 classes 方法后,能给 persistent classes 加类了。但下面的语句失败:

org.grails.datastore.mapping.model.AbstractMappingContext#addPersistentEntities() 中的
PersistentEntity entity = createPersistentEntity(javaClass);
失败
原来是没有实现 trait GormEntity。
将原来的 grails.persistence.Entity 替换为 grails.gorm.annotation.Entity 就可以了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值