Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Scaffold Controller and Service support #1

Open
wants to merge 3 commits into
base: 6.0.x
Choose a base branch
from

Conversation

codeconsole
Copy link

Adds support for the annotation@ScaffoldController which provides an alternative to the static scaffold = Domain approach.

@ScaffoldController(domain = User)
class UserController {}

It also empowers the developer to use their own base controller instead of being forced to use RestController which could be very limiting. RestController does not encapsulate all necessary business logic and is completely divergent from the Service MVC model that is generated by this plugin. With this annotation, developers can now use an extended RestController to fit their needs, or create an entirely different generic controller that is not related at all to RestController and more closely matches the type of controller that is generated by this plugin.

@ScaffoldController(GenericAsyncController)
class UserController {
    static scaffold = User
}
@ScaffoldController(value = SecuredGenericController, domain = User)
class UserController {}

grails/scaffolding#113

@rainboyan
Copy link
Member

rainboyan commented Aug 30, 2024

@codeconsole This is good, I will check it out later.

How about using @Scaffold annotation, it's simple, just like scaffold property.

Also, I prefer to use domain as the default value of @ScaffoldController, and base or baseController for the base controller?

public @interface @Scaffold {

    Class<?> value() default Void.class;
    Class<?> base() default RestController.class;

}

In addition, this new feature would be more appropriate in scaffodling 6.1.x, which is planned to be included in Grace 2023.1.0.

What do you think?

Have you tried using it like this?, I've been hoping for a little cooler syntactic sugar in Grails, Rails has a syntax like concerns, which feels great.

class UserController {

    static scaffold = [User, base: RestController]

}

@codeconsole
Copy link
Author

@rainboyan there is a reason behind the approach I chose.

  1. The annotation could be universal. I would eventually like to be able to do the following:

Application.groovy

@ScaffoldController(SecuredGenericController)
class Application {
}

Which would configure ALL Scaffold Controllers to use SecuredGenericController instead of RestController

  1. The reason why I defaulted the base to Void was so that if it was not set, the universal controller would be used, not RestController. It also allows overriding the universal controller.

  2. I prefer annotations over static properties.

  3. Using @scaffold instead of @ScaffoldController could cause conflicting annotations in the future. What if we wanted to scaffold other types of components?

  4. There is no need to use the word base when it is the default value for the Controller annotation.

  5. Adding domain eliminated the need to have static scaffold

  6. Using both ways combined is allowed

ScaffoldController(GenericAsyncController)
class UserController {
    static scaffold = User
}

@codeconsole
Copy link
Author

codeconsole commented Sep 1, 2024

I have some changes I am currently testing, but appear to be working:
I am also introducing the @ScaffoldService annotation

@ScaffoldController(RestController<User>)
class UserController {}

@ScaffoldService(GenericService<User>)
class UserService {}

@ScaffoldController(GenericController<User>)
class ImageController {}

@ScaffoldService(GenericService<Image>)
class ImageService {}

The domain attribute on the annotation is still available and takes precedence, but you do not have to provide it. It can be set using generics.

@rainboyan
Copy link
Member

I don't understand how the @ScaffoldService works, maybe it reduces amount of generated code, but it is not easy to read and understand. I think we should consider using OOP and design patterns to make it easier maintainable.

Could you provide some complete examples to explain how it works?

@codeconsole
Copy link
Author

codeconsole commented Sep 2, 2024

@rainboyan It is very powerful if you have common business logic shared across multiple or all services and use a service layer. It works similar to how scaffold controllers work. You have a super class that you define (such are RestfulContoller for scaffold controllers) and then any Services with the annotation get that super class injected.

For example, let's create a GenericService that automatically updates Redis and ElasticSearch:

class GenericService<T> {

    T get(Long id) {
        // generic get logic
    }

    List<T> list(Map args) {
        // generic list logic
    }

    Long count(Map args) {
        // generic count logic.  (My implementation takes a map so counts can be done based on parameters)
    }

    void delete(Long id) {
        // generic delete logic
    }

    T save(T domain) {
        // generic save logic
    }
}

Now all I have to do is have define a Service with the annotation and all those methods become implemented. Unlike @Service, I can override individual methods and call super

@ScaffoldService(GenericService<User>)
class UserService {

    User save(User user) {
         user.modified = new Date()
         super.save(user)
    }
}

@rainboyan
Copy link
Member

  1. The annotation could be universal. I would eventually like to be able to do the following:

Application.groovy

@ScaffoldController(SecuredGenericController)
class Application {
}

Which would configure ALL Scaffold Controllers to use SecuredGenericController instead of RestController

I don't think it's feasible, but I haven't tried it.

  1. The reason why I defaulted the base to Void was so that if it was not set, the universal controller would be used, not RestController. It also allows overriding the universal controller.

There is really no example of a global configuration of the Groovy AST.

  1. I prefer annotations over static properties.

The use of static properties is still the majority of options at present. But Groovy and Grails lack a clear definition of this part of the property.

  1. Using @scaffold instead of @ScaffoldController could cause conflicting annotations in the future. What if we wanted to scaffold other types of components?

Scaffolding seems to be referring specifically to Controllers and Views at the moment

  1. There is no need to use the word base when it is the default value for the Controller annotation.

@Resource has a property superClass, allow to specify the super class of the controller

@Resource(uri='/books', superClass=SubclassRestfulController)
class Book {
    String title

    static constraints = {
        title blank:false
    }
}
  1. Adding domain eliminated the need to have static scaffold
  2. Using both ways combined is allowed
ScaffoldController(GenericAsyncController)
class UserController {
    static scaffold = User
}

Have you tried this, whether this will meet your needs?

class UserController extends GenericAsyncController<User> {

     static scaffold = User

}
  1. Now all I have to do is have define a Service with the annotation and all those methods become implemented.
    Unlike @service, I can override individual methods and call super

If you come across a method that GORM doesn’t know how to implement, then you can provide an implementation by using an abstract class, and you can also override individual methods.

interface IBookService {
    Book getBook(Serializable id)
    Date someOtherMethod()
}
@Service(Book)
abstract class BookService implements IBookService {

   @Override
   Date someOtherMethod() {
      // impl
   }
}

@codeconsole
Copy link
Author

codeconsole commented Sep 4, 2024

@Resource has a property superClass, allow to specify the super class of the controller

@Resource(uri='/books', superClass=SubclassRestfulController)
class Book {
    String title

    static constraints = {
        title blank:false
    }
}

That will not render scaffold views from the ScaffoldViewResolver and is way too verbose.

Have you tried this, whether this will meet your needs?

class UserController extends GenericAsyncController<User> {

     static scaffold = User

}

That is not valid code and will throw an exception. Scaffold controllers can not have super classes.
Plus, this is WAY more concise.

@ScaffoldController(GenericAsyncController<User>)
class UserController {}

Scaffolding seems to be referring specifically to Controllers and Views at the moment

If that is true, what is this Service.groovy?

interface IBookService {
    Book getBook(Serializable id)
    Date someOtherMethod()
}
@Service(Book)
abstract class BookService implements IBookService {

   @Override
   Date someOtherMethod() {
      // impl
   }
}

That Is waay to verbose and you can not call super inside BookService. This is way more DRY:

class GenericService<T> {
    Class<T> resource
    String resourceName
    @Autowired DatastoreService datastore

    GenericService(Class<T> resource, boolean readOnly) {
        this.resource = resource
        this.readOnly = readOnly
    }

    T get(Serializable id) {
        T instance = resource.getDeclaredConstructor().newInstance()
        instance.properties << datastore.get(id)   
        instance
    }

    List<T> list(Map args) { 
         // implementation  ...
    }

    Long count() {}  // implementation  ...

    void delete(Serializable id) {}  // implementation  ...

    T save(T instance) //  // implementation  ...
}

@ScaffoldService(GenericService<User>)
class UserService {}

@ScaffoldController(RestfulController<User>) 
class UserController {}

@ScaffoldService(GenericService<Book>)
class BookService {
    Book save(Book book) {
         book.owner = request.user
         super.save(user)
    }
}

@ScaffoldController(RestfulController<Book>) 
class BookController {}

@ScaffoldService(GenericService<Car>)
class CarService {}

@ScaffoldController(RestfulController<Car>) 
class CarController {}

@ScaffoldService(value = PetService<Pet>, readOnly = true)
class PetService {}

@ScaffoldController(value = RestfulController<Pet>, readOnly = true) 
class PetController {}

class BootStrap {
    CarrService carService
    PetService petService
    UserService userService
    BookService bookService

    def init = { servletContext ->
         User rainboyan = userService.save(new User(username:'rainboyan')
         Car porsche = carService.save(new Car(make:'Porsche',  model:'911', owner: rainboyan)
         Book grails = bookService.save(new Book(name:'Programming Grails')
         Pet cat = petService.save(new Pet(name: 'Garfield', owner: rainboyan) // exception throw because service is readOnly
    }
}

@rainboyan
Copy link
Member

@codeconsole I created a example to demonstrate how to use super controller with a scaffolding controller.

https://github.com/rainboyan/grace-scaffolding-demo

@Resource(uri='/books', formats = ['html', 'json'], superClass=ScaffoldBookController)
class Book {

    String title

    static constraints = {
    }
}
class UserController extends SubclassRestfulController<User> {

    static scaffold = User

    UserDataService userDataService

    UserController() {
        super(User, false)
    }

    @Override
    protected User createResource(Map params) {
        return new User(params)
    }

    @Override
    protected User createResource() {
        User user = new User()
        bindData user, getObjectToBind()
        user
    }

    @Override
    protected User queryForResource(Serializable id) {
        return this.userDataService.get(id)
    }

    @Override
    protected Integer countResources() {
        return this.userDataService.count()
    }

    @Override
    protected User saveResource(User resource) {
        return this.userDataService.save(resource)
    }

    @Override
    protected void deleteResource(User resource) {
        this.userDataService.delete(resource.id as Serializable)
    }
}

@codeconsole
Copy link
Author

codeconsole commented Sep 7, 2024

@rainboyan I don't like it. There isn't a clear Separation of Concerns. The whole reason behind @ScaffoldController and @ScaffoldService is to keep code DRY while at the same time having a clear Separation of Concerns.

  1. There should not be a reference in Book to the web layer. A Book shouldn't even know what a BookController is.
  2. There is repeat logic that really doesn't need to exist.
  3. I think annotations are a better fit for defining compile time configuration. static attributes are not and can be modified during runtime which would result in unpredictable behavior.
  4. @ScaffoldController is a better fit replacement for static scaffold = Domain for several reasons. a) it allows replacing the superclass RestController. b) it allows defining the value hard coded for the readOnly attribute. c) it defines configuration for an AST transformation which is done at compile time.
  5. static scaffold = Book in your example does absolutely nothing other than resolve scaffold views.
  6. It still requires using RestfulController as a super class. I don't want to use anything related to RestfulController. My controller super class is a generic version of Controller.groovy.
class ScaffoldBookController extends RestfulController<Book> {

    static scaffold = Book

    ScaffoldBookController(Class<Book> resource) {
        super(resource)
    }

    ScaffoldBookController(Class<Book> resource, boolean readOnly) {
        super(resource, readOnly)
    }
}

can be replaced with

@ScaffoldController(RestfulController<Book>) 
class BookController {}

If you have 50 controllers, your version required maintaining a minimum of 200 more lines of code across 100 files..

If you are so pro RestfulController, maybe you should change Controller.groovy to

<%=packageName ? "package ${packageName}" : ''%>

class ${className}Controller extends RestfulController<${className}>{
}

@codeconsole
Copy link
Author

codeconsole commented Sep 7, 2024

Another option is since @ScaffoldControler and @ScaffoldService are the same and have the same properties, we could just have one annotation @Scaffold that works for both services and controllers.

class GenericService<T> {
    Class<T> resource
    String resourceName
    @Autowired DatastoreService datastore

    GenericService(Class<T> resource, boolean readOnly) {
        this.resource = resource
        this.readOnly = readOnly
    }

    T get(Serializable id) {
        T instance = resource.getDeclaredConstructor().newInstance()
        instance.properties << datastore.get(id)   
        instance
    }

    List<T> list(Map args) { 
         // implementation  ...
    }

    Long count() {}  // implementation  ...

    void delete(Serializable id) {}  // implementation  ...

    T save(T instance) //  // implementation  ...
}

@Scaffold(GenericService<User>)
class UserService {}

@Scaffold(RestfulController<User>) 
class UserController {}

@Scaffold(GenericService<Book>)
class BookService {
    Book save(Book book) {
         book.owner = request.user
         super.save(user)
    }
}

@Scaffold(RestfulController<Book>) 
class BookController {}

@Scaffold(GenericService<Car>)
class CarService {}

@Scaffold(RestfulController<Car>) 
class CarController {}

@Scaffold(value = PetService<Pet>, readOnly = true)
class PetService {}

@Scaffold(value = RestfulController<Pet>, readOnly = true) 
class PetController {}

class BootStrap {
    CarrService carService
    PetService petService
    UserService userService
    BookService bookService

    def init = { servletContext ->
         User rainboyan = userService.save(new User(username:'rainboyan')
         Car porsche = carService.save(new Car(make:'Porsche',  model:'911', owner: rainboyan)
         Book grails = bookService.save(new Book(name:'Programming Grails')
         Pet cat = petService.save(new Pet(name: 'Garfield', owner: rainboyan) // exception throw because service is readOnly
    }
}

@codeconsole codeconsole changed the title @ScaffoldController support @Scaffold Controller and Service support Sep 7, 2024
@codeconsole
Copy link
Author

codeconsole commented Sep 7, 2024

@rainboyan Here is what the final product would look like. Let me know if you want me to merge in the rest of the changes or you have any further suggestions. grails/scaffolding#118

@rainboyan
Copy link
Member

@codeconsole I have updated the demo, does it meets your requirements?

But currently it's not working, I will figure it out later, maybe it's related to the GORM side.

class UserController extends ScaffoldRestfulController<User> {

    static scaffold = User

    UserDataService userDataService

    UserController() {
        super(User, userDataService)
    }

}
abstract class ScaffoldRestfulController<T> extends RestfulController<T> {

    CrudDataService<T> crudDataService

    ScaffoldRestfulController(Class<T> domainClass, CrudDataService<T> crudDataService) {
        this(domainClass, false, crudDataService)
    }

    ScaffoldRestfulController(Class<T> domainClass, boolean readOnly, CrudDataService<T> crudDataService) {
        super(domainClass, readOnly)
        this.crudDataService = crudDataService
    }

    @Override
    protected T queryForResource(Serializable id) {
        return this.crudDataService.get(id)
    }

    @Override
    protected Integer countResources() {
        return this.crudDataService.count()
    }

    @Override
    protected T saveResource(T resource) {
        return this.crudDataService.save(resource)
    }

    @Override
    protected void deleteResource(T resource) {
        this.crudDataService.delete(resource?.id as Serializable)
    }

}

I don't think scaffolding plugin should provide dynamic GORM services, although it provides Service template. Because the business logic is the detail, scaffolding plugin can't do much.
You can modify the templates or write a custom script command to do this job.

The other thing is If you use Scaffolding in your application, the readOnly should always be false.

@codeconsole
Copy link
Author

codeconsole commented Sep 10, 2024

I don't think scaffolding plugin should provide dynamic GORM services, although it provides Service template. Because the business logic is the detail, scaffolding plugin can't do much. You can modify the templates or write a custom script command to do this job.

@Scaffold on a service isn't providing dynamic GORM services. It is providing exactly what the Service template is providing except it allows you to override the individual methods, introduce business logic and add super.* calls.

The other thing is If you use Scaffolding in your application, the readOnly should always be false.

Why should it always be false? What if you wanted to expose 2 scaffold controllers, 1 admin and 1 for developers as an api that should only be readonly? What if you have a data layer that doesn't accept writes? (I have such a case using a cloud datastore)

Why do this:

class UserController extends ScaffoldRestfulController<User> {

    static scaffold = User

    UserDataService userDataService

    UserController() {
        super(User, userDataService)
    }

}

when you can do this:

@Scaffold(RestfulServiceController<User>)
class UserController {}

or even this:

@Scaffold(SecuredNonRestfulServiceController<User>)
class UserController {}

which way is more DRY? your proposal or mine?

why not provide both ways and give the developer a choice?

@Scaffold is now part of Grails. It would be nice to make it part of Grace as well. Let me know if you would like me to update my pull request.

@rainboyan
Copy link
Member

@codeconsole Partially accepted.

See https://github.com/rainboyan/grace-scaffolding-demo/blob/main/README.md

In order to make the two ways consistent in use.

Only supports Scaffolding Controllers, maybe there will be a Scaffolding Service in the future.

@codeconsole
Copy link
Author

@rainboyan I am not a big fan of @Scaffold(User). I already use @Scaffold(SecuredServiceController<T>) and @Scaffold<CachingDatastoreService<T> heavily in my codebase and it works well the way it is. I have found it very useful for both services and controllers and we now have implemented scaffolding across namespaces. It doesn't need to be similar to static scaffold = User. I think the annotation value should be the super class because most users will not use the default super class. The super class should contain a lot of reusable business logic.

With the latest Grails you can now do the following:

package website
@Scaffold(GormService<User>)
class UserService {}

package website.admin
@Scaffold(RestfulServiceController<User>) 
@Secured('ROLE_ADMIN')
class UserController {
    static namespace = 'admin'
}

package website
@Scaffold(value = RestfulServiceController<User>, readOnly = true) 
class UserController {}
src/main/templates/(create/edit/show/index).gsp
src/main/templates/admin/(create/edit/show/index).gsp

@Scaffold does not have to replace static scaffold. You can still use the old way if you like. I suggest you adopt the same behavior as Grails to avoid confusion. I am more than happy to finish this pull request to get you in sync with what I did at Grails. Let me know if you want me to add the missing commits.

@rainboyan
Copy link
Member

rainboyan commented Sep 14, 2024

@codeconsole Thanks for your comments.
As you said, you prefer to use Java annotations. This may be due to differences in programming languages, but Grails is more inclined to use static properties in its design, as Grails does in the Domain Class.

class Post {
    static hasMany = [comments: Comment]

    static mapping = {
        table: 'posts'
    }
}

The Groovy language uses annotations, mostly used in Groovy AST transformations, such as,

import groovy.transform.ToString

@ToString
class Person {
    String firstName
    String lastName
}

Spring and Micronaut are frameworks with Java as the main language, and of course they mostly use Java annotations.
However, I think Grails or Grace using static properties is a way to maintain continuity in design, which may not be the best, but it is the recommended usage at the moment.

In the current version (Grace 2023.1.x), I don't plan to make major changes to the Scaffolding plugin, just to improve it (#2)
In the next Grace 2024.0.x and Scaffolding 7.x releases, a number of refactorings and enhancements are planned.

Anyway, thank you for your PRs.It's a pleasure to discuss with you.

@codeconsole
Copy link
Author

@rainboyan

The Groovy language uses annotations, mostly used in Groovy AST transformations, such as,

correct, that is why static scaffold = true should be an annotation. It is an AST transformation that adds a superclass and binds a domain object.

Grails has numerous other annotations that also do AST transformations.

Grails is built on top of Spring which now relies heavily on annotations.

Spring and Micronaut are frameworks with Java as the main language, and of course they mostly use Java annotations. However, I think Grails or Grace using static properties is a way to maintain continuity in design, which may not be the best, but it is the recommended usage at the moment.

static properties are legacy and poor design. They were not poor design when they were invented because annotations were not yet introduced. That is the only reason why they are there. I believe if Grails was created today, it would not use static properties at all. By choosing to use static properties over annotations, you are using design decisions of 2005.

In the current version (Grace 2023.1.x), I don't plan to make major changes to the Scaffolding plugin, just to improve it (#2) In the next Grace 2024.0.x and Scaffolding 7.x releases, a number of refactorings and enhancements are planned.

The changes I propose to you are not major changes. Again, they are optional functionality that doesn't have to be used, but greatly empowers the developer if they choose to use them. They promote good design by separating the service layer from the web layer.

You have done an amazing job with Grace, but the more you diverge from Grails the less likely you will gain adoption from the Grails community. It would be nice if Grace offered a seamless transition from Grails especially when Grails is starting to gain momentum again. People are hesitant to move a project to a new framework unless it offers non lock in and an escape route. It would also be amazing if you could contribute back into Grails some of the lessons you learned in Grace.

@rainboyan
Copy link
Member

@codeconsole

correct, that is why static scaffold = true should be an annotation. It is an AST transformation that adds a superclass and binds a domain object.

I don't agree with you, @Scaffold is not an annotation of AST transformation in this example.

Grails is built on top of Spring which now relies heavily on annotations.

Sometime, only when your Grails app outside Grails app root. For example, @Entity, @Controller. But Groovy DSL is so powerful and expressive that it doesn't need much annotations.

static properties are legacy and poor design. They were not poor design when they were invented because annotations were not yet introduced. That is the only reason why they are there. I believe if Grails was created today, it would not use static properties at all. By choosing to use static properties over annotations, you are using design decisions of 2005.

Grace Scaffolding 7.x will introduce the following ways to provide more configuration options, I think this's something you can‘t do with annotation.

class UserController {

    static scaffold = {
        fields: {
            exclude: ['id', 'createdDate']
        }
    }

}

People are hesitant to move a project to a new framework unless it offers non lock in and an escape route.

Perhaps Grace is for those who have never used Grails before, and the key to winning users is to lift the heavy burden of the past and embrace the future.

It would also be amazing if you could contribute back into Grails some of the lessons you learned in Grace.

It's nice to see Grails start over, but it's hard for me to have a lot of energy and interest in Grails, and I have a lot of new ideas to work on Grace.

@codeconsole
Copy link
Author

I don't agree with you, @Scaffold is not an annotation of AST transformation in this example.

How so? Then why are all the modifications being done with @AstTransformer ?

Grace Scaffolding 7.x will introduce the following ways to provide more configuration options, I think this's something you can‘t do with annotation.

class UserController {

    static scaffold = {
        fields: {
            exclude: ['id', 'createdDate']
        }
    }

}

Isn't that behavior already available on the domain class? Why can't an annotation do the same thing?

People are hesitant to move a project to a new framework unless it offers non lock in and an escape route.

It's nice to see Grails start over, but it's hard for me to have a lot of energy and interest in Grails, and I have a lot of new ideas to work on Grace.

I recommend you contribute in a way that promotes your own framework. Contribute to Grails things that would make it easier for Grails users to transition to Grace. You have the opportunity, especially now (before 7.0 is released), to do that. The more commonality between code bases, the more you can leverage commits/plugins from other people. There should be some inter compatibility between the 2 frameworks.

Take Apache Cordova for instance. It is kind of like Grails. A lot of legacy code and a huge community of plugins. Then look at Capacitor. Completely rewritten and amazing and all the plugins of Cordova run under Capacitor. It is an easy transition and Capacitor is well maintained.

I want you to succeed. I would love to migrate some of my apps to Grace, but right now it is not an easy task. It is easier for me to focus on making Grails better. Grace should extend the capabilities of Grails.
Your best candidates for adoption are Grails users or people with prior experience with Grails Attracting new developers is a much more difficult proposition.

Also, by contributing beneficial improvement to Grails, you gain more attention from the Grails community which is free adverstising to your framework.

@rainboyan
Copy link
Member

How so? Then why are all the modifications being done with @AstTransformer ?

AstTransformer is just a annotation marker only in Grails, not the same as Groovy AST transformation @ToString, @Builder. At the same time, I think this design has some flaws, which will be optimized in the next big version Grace 2024.

Isn't that behavior already available on the domain class? Why can't an annotation do the same thing?

Annotations can be simple, but complex situations are a bit inappropriate. Scaffold clearly needed a more flexible and complex design.

I recommend you contribute in a way that promotes your own framework. Contribute to Grails things that would make it easier for Grails users to transition to Grace.
I would love to migrate some of my apps to Grace, but right now it is not an easy task.

In my opinion, migrating from Grails to Grace is really feasible, given the size and complexity of the project's code. I don't recommend upgrading directly, as it requires a specific analysis of how many external plugins are used in the project, and in my opinion, most of them are unnecessary, they are not migrated to Spring Boot, or they can be replaced.

The more commonality between code bases, the more you can leverage commits/plugins from other people. There should be some inter compatibility between the 2 frameworks.

A lot of Grace plugins, Htmx, Hotwire, Policy, View Components will support both Grails and Grace.I think it's also a very good way to contribute to the Grails community.

Grace should extend the capabilities of Grails.

Since the release of Grails to 3.0, the transition would have been a success. But it was a shame that we didn't take the opportunity to grow with Spring Boot. Since then, technical debt has been holding back its growth, and the development team has not had the motivation or determination to push for the refactoring and upgrading of Grails, which has been abandoned by the community. Grace has been actively refactoring and improving these from the start, which may lead to a parting of ways with Grails.

Also, by contributing beneficial improvement to Grails, you gain more attention from the Grails community which is free adverstising to your framework.

As I've said before, the gap between Grails and Spring Boot is getting bigger and bigger, and it's time to break through.
At the moment, I think it's pretty easy if Grails 7 is just migrating to Groovy 4 and Spring Boot 3. James, Mattias and you can do a great job together.
But, especially in the current situation, Grails and Groovy need some innovation and experimentation, I do have some ideas of my own that need more time and energy to validate.

Next, I'll update the documentation, Guides, and I'll be happy to talk to the Grails team and share some of my views.

Thank you very much for your suggestions and contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants