the setup:
Module 1 an external dependency that has Class A
Module 2 depends on Class A but may in the future depends on another module/class entirely
an APP depends on Module 2
I tried to add a typealias in Module 2 to Class A. it exposes the class "correctly" (lets say it is com.moduleTwo.ClassA instead of com.moduleOne.ClassA) but it makes it so that APP also needs to have a dependency on Module 1 or it doesn't compile with:
Cannot access class 'com.moduleOne.ClassA'. Check your module classpath for missing or conflicting dependencies
How can one make Module 2 expose an alias to Class A without adding Module 1 to APP build.gradle? Is there a way to "inject" the dependency to APP?
If I understood correctly you would like to replace a module without having app's code depending on external libraries.
When your app's code depends on only the abstraction and not concrete implementation you can replace those dependencies at any time.
The piece of code below shows that User in app module depends on interface A but the real implementation of A may vary.
// library module A
interface A {
fun doWork()
}
// library module B implements modules A & External
class B(private val external : External) : A {
override fun doWork() {
val result = external.getExternalStuff()
// more work with result
}
}
object Factory {
fun createB(params...): B {
val external = External(paramX)
// ...
return B(external)
}
}
// library module C implements module A
class C : A {
override fun doWork() {
println("C works")
}
}
// app
// User knows only 'A'
class User(private val a: A) {
fun help() {
a.doWork()
}
}
// Version 1: App implements modules A & B
// app module will know nothing about External
// but some factory or builder class to create B
fun appStarts() {
val a = Factory.createB(params...)
val user = User(a)
user.help()
}
// Version 2: App implements modules A & C
fun appStarts() {
val a = C()
val user = User(a)
user.help()
}
Hope this helps.
Related
This is the error I get when I try to create the class under test
Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener()
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener() at com.pittacode.apihelper.json.JsonObjectFlattenerTest.flatten json object with one nested object(JsonObjectFlattenerTest.groovy:12)
This is my test class
package com.pittacode.apihelper.json
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import spock.lang.Specification
class JsonObjectFlattenerTest extends Specification {
def classUnderTest = new JsonObjectFlattener()
def "flatten json object with one nested object"() {
given:
def jsonString = """
{
"1-1": 11,
"1-2": {
"2-1": "21"
},
"1-3": 13
}
"""
def jsonObject = JsonParser.parseString(json).getAsJsonObject()
when:
JsonObject result = classUnderTest.flatten(jsonObject)
then:
result.keySet().containsAll(["1-1", "1-2", "1-3", "2-1"])
}
}
I have a gradle project with one subproject and a module-info.java
plugins {
id "groovy"
id "application"
id "org.beryx.jlink" version "2.25.0"
id "org.javamodularity.moduleplugin" version "1.8.10"
}
repositories {
mavenCentral()
}
ext {
log4jVersion = "2.17.2"
}
dependencies {
implementation("com.jayway.jsonpath:json-path:2.7.0") {
exclude group: "com.fasterxml.jackson.core"
exclude group: "com.google.gson"
}
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.2")
implementation("com.google.code.gson:gson:2.9.0")
implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-core:${log4jVersion}")
annotationProcessor("org.apache.logging.log4j:log4j-core:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
testImplementation("org.codehaus.groovy:groovy:3.0.9")
testImplementation("org.spockframework:spock-core:2.0-groovy-3.0")
}
application {
mainClass = "com.pittacode.apihelper.Runner"
mainModule = "com.pittacode.apihelper"
}
tasks.named("test") {
useJUnitPlatform()
}
jlink {
forceMerge "log4j", "jackson"
// options = ["--bind-services"] // makes jre bigger but has everything, good way to test stuff
launcher {
name = "apihelper"
jvmArgs = ["-Dlog4j.configurationFile=./log4j2.xml", "-Dlog4j2.debug=false"]
}
jpackage {
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ["--win-per-user-install", "--win-dir-chooser", "--win-menu", "--win-shortcut"]
imageOptions += ["--win-console"]
}
}
}
tasks.jlink.doLast {
copy {
from("src/main/resources")
into("$buildDir/image/bin")
}
}
This is the class
package com.pittacode.apihelper.json;
import com.google.gson.JsonObject;
public final class JsonObjectFlattener {
public JsonObjectFlattener() {
}
public JsonObject flatten(JsonObject o) {
return null;
}
}
The weird thing is that in another specification class if I try to initiate another object (unrelated) it seems to create it just fine. That one has parameters so I tried adding some in the flattener as well but didn't seem to make a difference
Well this was silly,
Like I mentioned I am using java modules and it seems that I need to export the packages that contain the classes I want to test.
module com.pittacode.apihelper {
requires jdk.crypto.ec; // needed for ssl communication
requires org.slf4j;
requires java.net.http;
requires java.sql;
requires com.google.gson;
requires json.path;
requires org.apache.logging.log4j;
requires com.fasterxml.jackson.databind;
exports com.pittacode.apihelper;
exports com.pittacode.apihelper.json; // <-- this is the missing line
}
I created a small project in Intellij Idea to mimic what I am seeing trying to migrate a large legacy codebase to OPENJDK16. Here is the directory structure:
$ tree
/cygdrive/c/projects/play
|--api
|----api.iml
|----src
|------module-info.java (module com.company.feature.apimodule)
|------com
|--------company
|----------feature
|------------ApiRunnable.java
|--home
|----home.iml
|----src
|------module-info.java (module com.company.feature.homemodule)
|------com
|--------company
|----------feature
|------------Runnable1.java
|--Modules
|----.idea (Idea stuff in here)
|----out
|------production
|--------api
|--------home
module com.company.feature.apimodule
{
exports com.company.feature;
}
package com.company.feature;
public interface ApiRunnable
{
public void play(String[] strings);
}
module com.company.feature.homemodule
{
requires com.company.feature.apimodule; //Error Module not found
// requires apimodule; //Error Module not found
// requires com.company.feature.ApiRunnable; //Error Module not found
}
package com.company.feature;
public class Runnable1 implements ApiRunnable
{
#Override
public void play(String[] strings)
{
int count = 1;
for (String str : strings)
{
System.out.println("String " + count++ + " " + str);
}
}
public static void main(String[] args)
{
Runnable1 myRun = new Runnable1();
myRun.play(args);
}
}
Note the directory structure is identical in api and home. This is intentional as my legacy codebase has this structure. This small example seems to duplicate the problems I am having and I can't figure out if the structure is the problem. It did compile and run fine using the unnamed-module.
Error #1 when doing a rebuild
C:\Projects\play\home\src\com\company\feature\Runnable1.java
java: package exists in another module: com.company.feature.apimodule
Does this mean that I can't have identical package trees in api and home?
Error #2
The homemodule cannot see the apimodule, I keep getting (Module not found) no matter what I put in the requires line.
Is this related to Error #1? If not, how do I fix this without resorting to the unnamed module for everything?
This problem was resolved by putting the module-info.java in the right directory. This is poorly documented and was found by trial and error.
The right directory is api:
module apimodule
{
exports com.company.feature;
}
and home:
module homemodule
{
requires apimodule;
}
It appears that once the module-info.java files are in place, the Runnable1.java doesn't need the import statements, since the imported package is the same as the local one.
I have a custom ClassLoader extending GroovyClassLoader which compiles the source code to .class files on disk and then loads the resulting class:
class MyClassLoader extends GroovyClassLoader {
File cache = new File( './cache' )
Compiler compiler
MyClassLoader() {
CompilerConfiguration cc = new CompilerConfiguration( targetDirectory:cache )
compiler = new Compiler( cc )
addClasspath cache.path
}
#Override
Class findClass( name ) {
try{
parent.findClass name
}catch( ClassNotFoundException e ){
compiler.compile name, getBodySomehow()
byte[] blob = loadFromFileSystem name
Class c = defineClass name, blob, 0, blob.length
setClassCacheEntry c
c
}
}
#Override
void removeClassCacheEntry(String name) {
Class c = cache[ name ]
super.removeClassCacheEntry(name)
GroovySystem.metaClassRegistry.removeMetaClass c
deleteFiles name
}
}
Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'
Now if I change the source code, call myClassLoader.removeClassCacheEntry(name) and try myClassLoader.loadClass() again I'm getting:
java.lang.LinkageError: loader (instance of com/my/MyClassLoader): attempted duplicate class definition for name some/pckg/SomeClass
I read the greater half of the Internet and found a "solution" to initialize a class-loader for each class:
MyClassLoader myClassLoader = new MyClassLoader()
Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'
This seems to be working but raises performance concerns of mine...
What is the proper way to reload classes? How can I reuse the same class-loader? What am I missing?
Actually there is a trick that could be used
Originally, when you call
classLoader.defineClass(className, classBytes, 0, classBytes.length)
It calls java native method defineClass1 that actually calls loadClass method.
So, possible to intercept this method and process it a bit different then original.
In the folder that contains cached class files I have the following groovy compiled to class: A.class
println "Hello World!"
B.class to check dependent class loading
class B extends A {
def run(){
super.run()
println "Hello from ${this.getClass()}!"
}
}
and C.class to check multi-level class loading
i used this jar to compile following class and run the class re-loading example
class C extends org.apache.commons.lang3.RandomUtils {
def rnd(){ nextInt() }
}
the following class + code loads and reloads the same class:
import java.security.PrivilegedAction;
import java.security.AccessController;
import org.codehaus.groovy.control.CompilationFailedException;
#groovy.transform.CompileStatic
class CacheClassLoader extends GroovyClassLoader{
private File cacheDir = new File('/11/tmp/a__cache')
private CacheClassLoader(){throw new RuntimeException("default constructor not allowed")}
public CacheClassLoader(ClassLoader parent){
super(parent)
}
public CacheClassLoader(Script parent){
this(parent.getClass().getClassLoader())
}
#Override
protected Class getClassCacheEntry(String name) {
Class clazz = super.getClassCacheEntry(name)
if( clazz ){
println "getClassCacheEntry $name -> got from memory cache"
return clazz
}
def cacheFile = new File(cacheDir, name.tr('.','/')+'.class')
if( cacheFile.exists() ){
println "getClassCacheEntry $name -> cache file exists, try to load it"
//clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
super.setClassCacheEntry(clazz)
}
return clazz
}
private PrivelegedLoader getPrivelegedLoader(){
PrivelegedLoader loader = AccessController.doPrivileged(new PrivilegedAction<PrivelegedLoader>() {
public PrivelegedLoader run() {
return new PrivelegedLoader();
}
});
}
public class PrivelegedLoader extends CacheClassLoader {
private final CacheClassLoader delegate
public PrivelegedLoader(){
super(CacheClassLoader.this)
this.delegate = CacheClassLoader.this
}
public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException {
Class c = findLoadedClass(name);
if (c != null) return c;
return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript, resolve);
}
}
}
def c=null
//just to show intermediate class loaders could load some classes that will be used in CacheClassLoader
def cl_0 = new GroovyClassLoader(this.getClass().getClassLoader())
cl_0.addClasspath('/11/tmp/a__cache/commons-lang3-3.5.jar')
//create cache class loader
def cl = new CacheClassLoader(cl_0)
println "---1---"
c = cl.loadClass('A')
c.newInstance().run()
println "---2---"
c = cl.loadClass('A')
c.newInstance().run()
println "---3---"
cl.removeClassCacheEntry('A')
c = cl.loadClass('A')
c.newInstance().run()
println "---4---"
c = cl.loadClass('B')
c.newInstance().run()
println "---5---"
cl.removeClassCacheEntry('A')
cl.removeClassCacheEntry('B')
c = cl.loadClass('B')
c.newInstance().run()
println "---6---"
c = cl.loadClass('C')
println c.newInstance().rnd()
result:
---1---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---2---
getClassCacheEntry A -> got from memory cache
Hello World!
---3---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---4---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> got from memory cache
Hello World!
Hello from class B!
---5---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
Hello from class B!
---6---
getClassCacheEntry C -> cache file exists, try to load it
226399895
PS: not sure priviledged access required
JVM does not allow to just unload some class, the only way to unload a class is to GC it. And class can be GC just like every other object -> all reachable references must be removed and GC run.
The tricky part is... class loader hold references to all classes. So the only way to unload a class is to get rid of both class and class loader.
You can find more information in language specification: https://docs.oracle.com/javase/specs/jvms/se13/jvms13.pdf 12.7 Unloading of Classes and Interfaces
An implementation of the Java programming language may unload classes.
A class or interface may be unloaded if and only if its defining class
loader may be reclaimed by the garbage collector as discussed in
§12.6. Classes and interfaces loaded by the bootstrap loader may not
be unloaded.
And class unloading does not need to be implemented at all in some JVM implementations:
Class unloading is an optimization that helps reduce memory use.
[...] system chooses to implement an optimization such as class
unloading. [...] Consequently, whether a class or interface has been unloaded
or not should be transparent to a program.
There is also explanation why class loader can't be reachable to unload class, as class might contain static variables and blocks of code that would be reset and executed again if this same class would be later loaded again. It's quite long and already a bit off topic so I will not paste it here.
So each your script should just use own class loader as that's the only way to actually not waste memory, so class can be GC later. Just make sure that you don't use any libraries that might cache references to your class - like many serialization/ORM libraries might do this for data types, or some other reflection libraries.
Another solution would be to use different scripting language that does not create java classes and just execute some kind of AST structure.
There is also one more solution to this problem, but it is very tricky and it's not something you should use on production, it even requires you to provide special JVM arguments or JVM from JDK that contains all needed modules. As java supports instrumentation API that can allow you to change bytecode of class at runtime, but if class is already loaded you can only change bytecode of methods, you can't add/remove/edit method/field/class signatures. So it could be very bad idea to use it for such scripts, so I will stop here.
I know probably there is no clear answer for this question.
But I would like to know Your opinions and maybe new ideas.
I'm wondering which of the following options is the best/right/correct way to build the app-level Dagger Component in Application class.
Example 1:
public class MyApp extends Application {
private NetComponent mNetComponent;
#Override
public void onCreate() {
super.onCreate();
mNetComponent = DaggerNetComponent.builder()
.appModule(new AppModule(this))
.netModule(new NetModule("https://api.github.com"))
.build();
}
public NetComponent getNetComponent() {
return mNetComponent;
}
}
Usage:
((MyApp) getApplication()).getNetComponent().inject(this);
Example 2:
class MyApplication extends Application {
private static MyComponent component;
#Override
void onCreate() {
component = DaggerMyComponent.builder()
.contextModule(new ContextModule(getApplicationContext()))
.build();
}
public static MyComponent getMyComponent() {
return component;
}
}
Usage:
MyApplication.getMyComponent().inject(this)
Example 3:
class CustomApplication: Application() {
lateinit var component: SingletonComponent
private set
override fun onCreate() {
super.onCreate()
INSTANCE = this
component = DaggerSingletonComponent.builder()
.contextModule(ContextModule(this))
.build()
}
companion object {
private var INSTANCE: CustomApplication? = null
#JvmStatic
fun get(): CustomApplication = INSTANCE!!
}
}
Then:
class Injector private constructor() {
companion object {
#JvmStatic
fun get() : SingletonComponent = CustomApplication.get().component
}
}
Usage:
Injector.get().catRepository()
Example 4:
class App : Application() {
var repositoryComponent: RepositoryComponent? = null
var appComponent: AppComponent? = null
override fun onCreate() {
super.onCreate()
instance = this
appComponent = DaggerAppComponent.builder().application(this).build()
repositoryComponent = DaggerRepositoryComponent.builder().build()
}
companion object {
private var instance: App? = null
fun get(): App {
return instance!!
}
}
}
Usage:
App.get().repositoryComponent!!.inject(this)
What do you think about this? Is there any better / cleaner way to do this? Maybe provided examples are fine? Or maybe just one of them?
I will be grateful for any good examples / tips / advices.
Thanks!
Okay, no one answered in 5 days so it's my turn, despite my bias :p
Option #1
((MyApp) getApplication()).getNetComponent().inject(this);
It's an "ok" version of doing things, except for two things.
First, the name. NetComponent isn't really for networking, it's the app-global singleton component, so it should be either called SingletonComponent or AppComponent. But naming it NetComponent is disingenuous, it's typically responsible for everything else too.
Second problem is that you need a reference to Context to access your dependency graph, making Context actually be a dependency rather than it being provided to you.
Option #2
MyApplication.getMyComponent().inject(this)
This is a perfectly fine way of doing things, but you need to know that to reach your object graph, you need to access the static method of MyApplication.
Option #3
Injector.get().inject(this)
Internally, this solution actually just calls over to get the app component, public static AppComponent get() { return MyApplication.getInstance().getComponent(); }
The benefit is that getComponent() is exposed via an instance method of Application, so it could be theoretically swapped out.
Also, invoking a method on something called Injector.get() is more obviously an "injector" than, well, an application class.
As for whether you use .catRepository() or .inject(this), it's up to you; but I personally prefer calling the provision methods to get the deps in Activity/Fragment, because listing the member-injection targets adds a lot of clutter to the component over time.
4.)
App.get().repositoryComponent!!.inject(this)
You can ditch the !! if repositoryComponent is a lateinit var.
Having two components for the same scope (and therefore two different object graphs) will only cause trouble, out of all of the options, this is the worst.
In my opinion, the 3rd option is the best. Technically it's the same as option #2 with an additional "indirection" through the instance method of Application that actually returns the component.
There is plenty of features that already available on Jeta, but what if something is missing. Can I create my own annotations and generate metacode for them?
Needed a step-by-step tutorial how to create custom Jeta processors.
How to create custom processors, step-by-step tutorial
Step 1: Hello, World project
For this tutorial let's create a simple Gradle project with one module app and with a single class SayHelloApp. This class writes Hello, World! to standard output.
For the illustration we are going to create Hello annotation that sets Hello, Jeta! string to the annotated fields.
Step 2: common module
First, we need a module that will be accessible in app and apt (will create shortly) modules. In common module we need two classes - Hello annotation and HelloMetacode interface:
Step 3: apt module
apt - is a module in which we'll create all the required for code generation classes. For this tutorial we need a processor that will handle our Hello annotation.
Note that this module depends on common module so we used Hello annotation as a parameter for the super constructor. By doing that we're saying to Jeta that we need all the elements annotated with given type. The module also depends on jeta-apt in order to get access to the Jeta classes.
Step 4: Processor
Created SayHelloProcessor now does nothing. Let's add some logic in it. The idea here is to generate java code that sets Hello, Jeta string to the fields annotated with Hello.
Note that Jeta uses JavaPoet to create java source code. It's really great framework by Square. Please, check it out on GitHub.
First, we need that our metacode implements HelloMetacode. To do that we'll add super interface to the builder:
MetacodeContext context = roundContext.metacodeContext();
ClassName masterClassName = ClassName.get(context.masterElement());
builder.addSuperinterface(ParameterizedTypeName.get(
ClassName.get(HelloMetacode.class), masterClassName));
Next, implement HelloMetacode by creating void setHello(M master) method:
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("setHello")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(masterClassName, "master");
Finally, the statements for each element annotated with Hello, that Jeta passes in process method via roundContext parameter:
for (Element element : roundContext.elements()) {
String fieldName = element.getSimpleName().toString();
methodBuilder.addStatement("master.$L = \"Hello, Jeta\"", fieldName);
}
Here is the complete SayHelloProcessor listing:
package org.brooth.jeta.samples.apt;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import org.brooth.jeta.apt.MetacodeContext;
import org.brooth.jeta.apt.RoundContext;
import org.brooth.jeta.apt.processors.AbstractProcessor;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
public class SayHelloProcessor extends AbstractProcessor {
public SayHelloProcessor() {
super(Hello.class);
}
#Override
public boolean process(TypeSpec.Builder builder, RoundContext roundContext) {
MetacodeContext context = roundContext.metacodeContext();
ClassName masterClassName = ClassName.get(context.masterElement());
builder.addSuperinterface(ParameterizedTypeName.get(
ClassName.get(HelloMetacode.class), masterClassName));
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("setHello")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(masterClassName, "master");
for (Element element : roundContext.elements()) {
String fieldName = element.getSimpleName().toString();
methodBuilder.addStatement("master.$L = \"Hello, Jeta\"", fieldName);
}
builder.addMethod(methodBuilder.build());
return false;
}
}
Step 5: Metacode
All the required for code generating classes are created and we're ready to try. But first, we need to add jeta.properties file in order to configurate Jeta. You can find more details about this file on this page. The file should be located in the root package. For our tutorial its content would be:
metasitory.package=org.brooth.jeta.samples
processors.add=org.brooth.jeta.samples.apt.SayHelloProcessor
Next, modify SayHelloApp. Instead of initializing text field we'll put Hello annotation on it:
public class SayHelloApp {
#Hello
String text;
}
And build.gradle:
group 'org.brooth.jeta-samples'
version '1.0'
buildscript {
repositories {
maven {
url 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath 'net.ltgt.gradle:gradle-apt-plugin:0.5'
}
}
apply plugin: 'net.ltgt.apt'
apply plugin: 'java'
sourceCompatibility = 1.7
repositories {
mavenCentral()
jcenter()
}
compileJava {
options.sourcepath = files('src/main/java')
}
dependencies {
apt project(':apt')
compile project(':common')
compile 'org.brooth.jeta:jeta:+'
}
Now we're ready to generate metacode. Run next command in your console:
./gradlew assemble
If there is no problems so far, we'll see SayHelloApp_Metacode file under app/build directory:
Step 6: Controller
Controllers are the classes that apply metacode to the masters. Let's create one for HelloMetacode in app module:
package org.brooth.jeta.samples;
import org.brooth.jeta.MasterController;
import org.brooth.jeta.metasitory.Metasitory;
public class SayHelloController<M> extends MasterController<M, HelloMetacode<M>> {
public SayHelloController(Metasitory metasitory, M master) {
super(metasitory, master, Hello.class, false);
}
public void setHello() {
for (HelloMetacode<M> metacode : metacodes)
metacode.setHello(master);
}
}
Step 7: MetaHelper
MetaHelper is a simple static-helper class. You shouldn't use it in your project if you are not comfortable with static helpers. You can read more details about this class on this page.
Anyway, let's create MetaHelper in app module:
package org.brooth.jeta.samples;
import org.brooth.jeta.metasitory.MapMetasitory;
import org.brooth.jeta.metasitory.Metasitory;
public class MetaHelper {
private static MetaHelper instance;
private final Metasitory metasitory;
public static MetaHelper getInstance() {
if (instance == null)
instance = new MetaHelper("org.brooth.jeta.samples");
return instance;
}
private MetaHelper(String metaPackage) {
metasitory = new MapMetasitory(metaPackage);
}
public static void setHello(Object master) {
new SayHelloController<>(getInstance().metasitory, master).setHello();
}
}
Note that we must pass to MapMetasitory the same package ("org.brooth.jeta.samples") that we specified as metasitory.package in jeta.properties.
Step 8: Usage
The last step - we invoke our MetaHelper's method. Here is the complete listing of SayHelloApp:
package org.brooth.jeta.samples;
public class SayHelloApp {
#Hello
String text;
public SayHelloApp() {
MetaHelper.setHello(this);
}
public void sayHello() {
System.out.print(text);
}
public static void main(String[] args) {
new SayHelloApp().sayHello();
}
}
Finally, we can run SayHelloApp. In the console we should see:
Hello, Jeta
Links
This tutorial on GitHub
Jeta Website
Jeta on Android
Happy code-generating! :)