Tuesday, September 18, 2018

Micronaut for Java EE/Jakarta EE Developers

There is a new microservices framework in town called Micronaut.  In this post, I'll discuss how I've ventured into working with the Micronaut framework from a Java EE/Jakarta EE perspective.  I am a Java EE developer, so developing microservices using solutions such as Eclipse MicroProfile are closer to my line of expertise, but Micronaut caught my attention since it offers the following abilities:

- Develop in Java, Groovy, or Kotlin
- Easy to test..fully integrated testing with Spock or JUnit
- Embedded Server & Compile Time HTTP Client
- Easy packaging for Docker
- Fast startup time, low memory consumption
- Fully Reactive

As an enterprise developer at heart, my first thought usually goes to the database, as the majority of applications that I author utilize an RDBMS.  I found the number of examples using Micronaut with an RDBMS to be few and far between, so I thought it may be useful for me to create another example for that use case.  In this example, I utilize PostgreSQL.  However, most other RDBMS are also supported.  This article is not meant to be a full explanation of installing Micronaut or utilizing all of the many Micronaut features.  Instead, it is a primer for those looking to get started utilizing Micronaut with a relational database...particularly geared towards those with some Java EE/Jakarta EE background.

In my particular case, I am interested in quickly spinning up Microservices that are fully testable, extensible, and efficient.  Although I can do this with MicroProfile or standard Java EE, I thought it would be interesting to learn something new and also have the ability to utilize Groovy or Kotlin.  I also wanted to put a Java EE/Jakarta EE spin on it...so I'm using JPA for working with the data.  Many of the Micronaut examples utilize Groovy and GORM for persistence...but I likely wouldn't be using that in any of my applications.

The example was developed using Apache NetBeans 9.0 and the Command Line Interface (CLI) that comes packaged with Micronaut.  This particular example was written against Micronaut 1.0.0.M4.  In this case, I kept it simple and utilized only a single, basic database table for persistence within a PostgreSQL database.

To begin, I created an app by utilizing the CLI by issuing the following command:


mn create-app org.acme.books --features hibernate-jpa,jdbc-tomcat

This simply creates a skeleton for my app within a directory named "books", and the Application.java main class will be placed within the org.acme.books package.  By default, there are basic features supported by an application, but in this case I've added support for the Tomcat connection pool.  This will be utilized when creating database connections via the Java Persistence API (JPA).  The default application is also generated with support for the Gradle build system.  Therefore, a build.gradle is created, and that is the file in which dependency management will take place.  Note that an application can also be generated utilizing the Apache Maven build system, but I had issues running Maven projects under Micronaut 1.0.0.M4...so I stuck with Gradle for this example.

If using Apache NetBeans 9.0, you can install the "Groovy and Grails" and "Gradle" plugins (currently available in the NetBeans 8.2 plugin center) to provide support for opening the project.  Once this is completed, the project can be opened within NetBeans and development can begin.  After installing the plugins and opening the project within Apache NetBeans, the completed project structure should look like that in the following figure:



To provide support for the PostgreSQL database, I added the dependencies to build.gradle:

compile group: 'org.postgresql', name: 'postgresql', version: '42.2.5'

Next, I opened up the application.yml file and added a datasource for the application.  This is the file that takes place of a persistence.xml within a traditional Java EE application.  Also, JPA support is added via this file, indicating which package includes the entity classes, as well as configuration of Hibernate.  Port 8080 is also set, as by default Micronaut will choose a random port on which to start the server.  The full sources of application.xml are as follows:

micronaut:
    application:
        name: books
                
#Uncomment to set server port
    server:
        port: 8080

---
datasources:
    default:
        url: jdbc:postgresql://localhost/postgres
        username: postgres
        password: yourpassword
        driverClassName: org.postgresql.Driver
        connectionTimeout: 4000
jpa:
  default:
    packages-to-scan:
        - 'org.acme.domain'
    properties:
      hibernate:
        hbm2ddl:
          auto: update
          show_sql: true

Now that the configuration is out of the way, I can get to the fun part...development.  In this example, I create a basic service allowing one to create, read, update, or delete records in the BOOK table.  The automatically generated Application class within the org.acme package, which starts the service.

package org.acme;

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}

To begin development, create two packages within the application for organizing the source code.  First, create org.acme.domain, which will contain the entity class.  Next, create org.acme.book, which will contain the implementation classes.  Create a Book.java class within the org.acme.domain package, which will be the entity class containing a standard JPA mapping for the database.  In this case, note that I utilize java.time.LocalDate for the date fields, and I utilize a database sequence generator for population of the primary key.  The sources are as follows:


package org.acme.domain;

import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;

/**
 * JPA Mappings for the BOOK database table.
 */
@Entity
@Table(name="BOOK")
public class Book {
    
    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE,
    generator="book_generator")
    @SequenceGenerator(name="book_generator",sequenceName="book_s", allocationSize=1)
    private Long id;
    
    @Column(name="PUBLISH_DATE")
    @NotNull
    private LocalDate publishDate;
    
    @Column(name="TITLE")
    @NotNull
    private String title;
    
    @Column(name="AUTHOR_FIRST")
    @NotNull
    private String authorFirst;
    
    @Column(name="AUTHOR_LAST")
    @NotNull
    private String authorLast;
    
    private Long pages;
    
    public Book(){}
    
    public Book(@NotNull Long id, @NotNull LocalDate publishDate, @NotNull String title, String authorFirst, String authorLast, Long pages){
        this.id = id;
        this.publishDate = publishDate;
        this.title = title;
        this.authorFirst = authorFirst;
        this.authorLast = authorLast;
        this.pages = pages;
    }

    public Book(@NotNull LocalDate publishDate, @NotNull String title, String authorFirst, String authorLast, Long pages){
        this.publishDate = publishDate;
        this.title = title;
        this.authorFirst = authorFirst;
        this.authorLast = authorLast;
        this.pages = pages;
    }

    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the publishDate
     */
    public LocalDate getPublishDate() {
        return publishDate;
    }

    /**
     * @param publishDate the publishDate to set
     */
    public void setPublishDate(LocalDate publishDate) {
        this.publishDate = publishDate;
    }

    /**
     * @return the title
     */
    public String getTitle() {
        return title;
    }

    /**
     * @param title the title to set
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * @return the authorFirst
     */
    public String getAuthorFirst() {
        return authorFirst;
    }

    /**
     * @param authorFirst the authorFirst to set
     */
    public void setAuthorFirst(String authorFirst) {
        this.authorFirst = authorFirst;
    }

    /**
     * @return the authorLast
     */
    public String getAuthorLast() {
        return authorLast;
    }

    /**
     * @param authorLast the authorLast to set
     */
    public void setAuthorLast(String authorLast) {
        this.authorLast = authorLast;
    }

    /**
     * @return the pages
     */
    public Long getPages() {
        return pages;
    }

    /**
     * @param pages the pages to set
     */
    public void setPages(Long pages) {
        this.pages = pages;
    }
   
    @Override
    public String toString() {
        return "Book{" +
            "id=" + id +
            ", publishDate='" + publishDate + '\'' +
            ", title='" + title + '\'' +
            ", authorFirst='" + authorFirst + '\'' +
            ", authorLast='" + authorLast + '\'' +
            ", pages='" + pages +
            '}';
    }
}

In a Micronaut application, HTTP requests and responses need to be encapsulated in Serializable classes for processing, and therefore it makes sense to generate some simple "Plain Old Java Objects" (POJOs) for encapsulating the data that will be used within database operations.  In the same org.acme.domain package, I created two such classes, BookSaveOperation.java and BookUpdateOperation.java.  These classes will define the fields required for passing data from the HTTP request to the controller class.  The sources for BookSaveOperation.java are as follows (see the GitHub repository for full sources):


package org.acme.domain;

import java.time.LocalDate;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 *
 * @author Josh Juneau
 */
public class BookSaveOperation implements java.io.Serializable {
    
    
    @NotNull
    private LocalDate publishDate;
    
    @NotNull
    @NotBlank
    private String title;
    
    @NotNull
    @NotBlank
    private String authorFirst;
    
    @NotNull
    @NotBlank
    private String authorLast;
    
    private Long pages;
    
    public BookSaveOperation(){}
    
    public BookSaveOperation(LocalDate publishDate, String title,
                    String authorFirst, String authorLast, Long pages){
        this.publishDate = publishDate;
        this.title = title;
        this.authorFirst = authorFirst;
        this.authorLast = authorLast;
        this.pages = pages;
    }

   // ...
   // getters and setters
   // ...
}

The application business logic occurs within a class which is much like an EJB or DAO implementation, and the class must implement an interface that has defined each of the business logic methods.  In this case, I created an interface org.acme.book.BookRepository.java, and define a few standard operational methods:

package org.acme.book;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.acme.domain.Book;

/**
 *
 */
public interface BookRepository {
    Book save(LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages);

    Optional<Book> findById(Long id);

    void deleteById(Long id);

    List<Book> findAll();

    int update(Long id, LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages);
}

Next, implement that interface within a class entitled org.acme.book.BookRepositoryImpl.java, and annotate as a @Singleton.  Since this is the class which will implement business logic, inject a PersistenceContext, which provides the JPA EntityManager that will be used for performing database operations.  Simply implement each of the operations outlined within the BookRepository interface, marking each with @Transactional (io.micronaut.spring.tx.annotation.Transactional), implying read only for those methods that will not modify any data.  The sources for BookRepositoryImpl.java are as follows:


package org.acme.book;

import io.micronaut.configuration.hibernate.jpa.scope.CurrentSession;
import io.micronaut.spring.tx.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.acme.domain.Book;

/**
 * Business logic for the service.
 */
@Singleton
public class BookRepositoryImpl implements BookRepository {
    @PersistenceContext
    private EntityManager entityManager;

    public BookRepositoryImpl(@CurrentSession EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    @Override
    @Transactional
    public Book save(LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages) {
        Book book = new Book(publishDate, title, authorFirst, authorLast, pages);
        entityManager.persist(book);
        return book;
    }

    @Override
    @Transactional(readOnly = true)
    public Optional<Book> findById(Long id) {
        return Optional.ofNullable(entityManager.find(Book.class, id));
    }

    @Transactional(readOnly = true)
    public List<Book> findAll() {
        return entityManager
                .createQuery("SELECT b FROM Book b", Book.class)
                .getResultList();
    }

    @Override
    @Transactional
    public int update(Long id, LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages) {
        return entityManager.createQuery("UPDATE Book b SET publishDate = :publishDate, title = :title, " +
                "authorFirst = :authorFirst, authorLast = :authorLast, pages = :pages where id = :id")
                .setParameter("publishDate", publishDate)
                .setParameter("title", title)
                .setParameter("authorFirst", authorFirst)
                .setParameter("authorLast", authorLast)
                .setParameter("pages", pages)
                .setParameter("id", id)
                .executeUpdate();
    }

    @Override
    @Transactional
    public void deleteById(Long id) {
        findById(id).ifPresent(book -> entityManager.remove(book));
    }

}


In an effort to explain the Micronaut application infrastructure from a Java EE perspective, I'll compare the implementation with a simple JAX-RS application.  Micronaut utilizes io.micronaut.http.annotation.Controller classes to perform the request-response handling for a service.  This is much like a JAX-RS controller class, with a few slight differences.  This very much reminds me of the Eclipse Krazo project, or MVC 1.0 for Java EE.  For instance, instead of annotating methods with the JAX-RS annotations javax.ws.rs.GET, javax.ws.rs.POST, or javax.ws.rs.Path, Micronaut uses io.micronaut.http.annotation.Get and io.micronaut.http.annotation.Post, among others.  The URI path for each of the methods can be directly declared via the @Get, @Post, @Put, @Delete annotations.  Each controller class will implement the functionality for the service and handles the request-response life cycle.  The business logic for persistence (contained within the BookRepositoryImpl class) is injected into the controller class via the @Inject annotation or via constructor injection.  In the sources for this example, constructor injection is used.

package org.acme.book;

import org.acme.domain.Book;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.validation.Validated;

import javax.validation.Valid;
import java.net.URI;
import java.util.List;
import org.acme.domain.BookSaveOperation;
import org.acme.domain.BookUpdateOperation;

@Validated
@Controller("/books")
public class BookController {

    protected final BookRepository bookRepository;

    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Get("/")
    public List<Book> list() {
        return bookRepository.findAll();
    }

    @Put("/")
    public HttpResponse update(@Body @Valid BookUpdateOperation operation) {

        bookRepository.update(operation.getId(), operation.getPublishDate(),
                operation.getTitle(), operation.getAuthorFirst(), operation.getAuthorLast(), operation.getPages());
        return HttpResponse.noContent().header(HttpHeaders.LOCATION, location(operation.getId()).getPath());

    }

    @Get("/{id}")
    Book show(Long id) {
        return bookRepository
                .findById(id)
                .orElse(null);
    }

    @Delete("/{id}")
    HttpResponse delete(Long id) {
        bookRepository.deleteById(id);
        return HttpResponse.noContent();
    }

    @Post("/")
    HttpResponse<Book> save(@Body @Valid BookSaveOperation operation) {

        Book book = bookRepository.save(operation.getPublishDate(), operation.getTitle(),
                operation.getAuthorFirst(), operation.getAuthorLast(), operation.getPages());
        return HttpResponse
                .created(book)
                .headers(headers -> headers.location(location(book)));
    }

    protected URI location(Book book) {
        return location(book.getId());
    }

    protected URI location(Long id) {
        return URI.create("/books/" + id);
    }

}

Testing the Application


Micronaut provides easy testing with Spock or JUnit and an embedded server...making it easy to create tests for each of the controllers.  In this case, I utilize JUnit to test the application.  I created a testing class within the test folder of the project named org.acme.BookControllerTest.

package org.acme;

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.acme.domain.Book;
import org.acme.domain.BookSaveOperation;
import org.acme.domain.BookUpdateOperation;
import org.junit.AfterClass;
import static org.junit.Assert.assertEquals;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Test cases for BookController
 */
public class BookControllerTest {

    private static EmbeddedServer server;
    private static HttpClient client;
    private  Book book;

    HttpRequest request;
    HttpResponse response;
    Long id;
    List<Long> bookIds = new ArrayList<>();

    @BeforeClass
    public static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class);
        client = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
    }

    @AfterClass
    public static void stopServer() {
        if (server != null) {
            server.stop();
        }
        if (client != null) {
            client.stop();
        }
    }

    @Test
    public void testInsertBooks() {

        request = HttpRequest.POST("/books", new BookSaveOperation(LocalDate.now(), "Java EE 8 Recipes", "Josh", "Juneau", new Long(750)));
        response = client.toBlocking().exchange(request);

        assertEquals(HttpStatus.CREATED, response.getStatus());

        request = HttpRequest.POST("/books", new BookSaveOperation(LocalDate.now(), "Java 9 Recipes", "Josh", "Juneau", new Long(600)));
        response = client.toBlocking().exchange(request);
        id = entityId(response, "/books/");

        assertEquals(HttpStatus.CREATED, response.getStatus());

       
    }

    @Test
    public void testBookRetrieve() {
        request = HttpRequest.GET("/books");
        List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
        // Populate a book instance for later
        for(Book b:books){
            book = b;
        }
        assertEquals(2, books.size());

        

    }
    
    @Test
    public void testBookOperations() {
        request = HttpRequest.GET("/books");
        List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
        // Populate a book instance for later
        for(Book b:books){
            book = b;
        }
        
        request = HttpRequest.PUT("/books/", new BookUpdateOperation(book.getId(),
                book.getPublishDate(),
                "Java 10 Recipes",
                book.getAuthorFirst(),
                book.getAuthorLast(),
                book.getPages()));

        response = client.toBlocking().exchange(request);

        assertEquals(HttpStatus.NO_CONTENT, response.getStatus());

        request = HttpRequest.GET("/books/" + book.getId());
        book = client.toBlocking().retrieve(request, Book.class);
        assertEquals("Java 10 Recipes", book.getTitle());

        testDelete();

    }
    
    
    public void testDelete(){
       request = HttpRequest.GET("/books");
        List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
        // Populate a book instance for later
        for(Book b:books){
            request = HttpRequest.DELETE("/books/" + b.getId());
            response = client.toBlocking().exchange(request);
            assertEquals(HttpStatus.NO_CONTENT, response.getStatus());
        }
    }

    Long entityId(HttpResponse response, String path) {
        String value = response.header(HttpHeaders.LOCATION);
        if (value == null) {
            return null;
        }
        int index = value.indexOf(path);
        if (index != -1) {
            return Long.valueOf(value.substring(index + path.length()));
        }
        return null;
    }
}

Walk-Through of the Logic in Testing Class


In the method that runs @BeforeClass, the HTTP server and client are created.  Similarly, when the tests have finished executing, the method that is annotated with @AfterClass is invoked, which stops the server if it is running.

From within the textInsertBooks() method, two new book records are created by passing new BookSaveOperation objects populated with data to the service that is available via the "/books" path with the @Post designation.  In this case, the controller method  BookController.save() is invoked.  Taking a look at the save() method, you can see that the method simply passes the contents of BookSaveOperation to the BookRepository.save() business method (utilizing the interface), persisting the object.  In the end, an HttpResponse is returned.

The testBookRetrieve() method calls upon the service that is available via the "/books" path containing the @Get designation.  This, in-turn, calls upon the BookController.list() method, which performs a findAll() on the BookRepository, returning a List of Book objects.

The testBookOperations() method is responsible for performing updates to the records.  First, the list of Book objects is retrieved from the BookController, and then one of the books is updated via the BookController.update() method by populating a BookUpdateOperation object with the contents to be updated.

** Keep in mind, the BookSaveOperation.java and BookUpdateOperation.java objects are simply POJOs that are used to move the data

Lastly, the testDelete() method is invoked, which traverses the List of Book objects, calling upon the BookController.delete() method via the service call to the "/books" path and invoking the method designated as @Delete.

To execute the tests, simply right-click the project in NetBeans and choose "Test", or use the command line to invoke using the following command

./gradlew test

If the database table has not yet been created, then it will be generated for you.  Note that you may need to modify the database configuration within application.yml accordingly for your environment.

Running the Service

Micronaut is self-contained, allowing a service to be executed using the embedded server which is built on Netty.  This can be done by right-clicking the project in Apache NetBeans and selecting "Run".  You could also go to the command line and invoke using the following command:

./gradlew run

You should see the URL on which the server is hosted displayed within the output in the terminal or Apache NetBeans output panel.

Summary


As a Java EE developer, I have to say that Micronaut does have a different development approach.  It is not too much different than that of Spring Boot or Grails, but it is different enough that it took me some time to find my way around.  In the end, I find it an easy to use framework that has a lot of potential for quickly developing services or scheduled tasks, while still harnessing some of the concepts from Java EE/Jakarta EE.  

There are a number of features that I've not played much with yet, such as creating scheduled tasks via the Micronaut framework, and developing using Groovy or Kotlin, rather than Java.  I hope to follow up to this post in the future with more information for Java EE and Jakarta EE developers who are interested in beginning their journey with Micronaut.

Friday, June 22, 2018

Apache NetBeans 9.0 - How to Build & Run the Latest

In my last post, I spoke about how to obtain the Release Candidate of Apache NetBeans 9.0.  There have been some changes made since the Release Candidate (including the addition of a very nice updated splash screen), so in this post I will cover how to build and run the latest sources.  I really is quite simple to run the latest code...here's how:

1)  Clone the latest source code from the GitHub repository to your local machine:

git clone https://github.com/apache/incubator-netbeans.git

2)  Open your terminal and traverse inside of the cloned directory named "incubator-netbeans"

3)  Build the IDE using Apache Ant.  If you do not have it installed, please download from here: https://ant.apache.org/

To build, simply issue the "ant" command from within the "incubator-netbeans" directory.



4)  The build process will take several minutes, as it obtains all of the dependencies and performs the compilation process.  Once completed, open the incubator-netbeans/nbbuild directory, and you should see a directory entitled "netbeans".  Inside this directory are all of the files required to run the Apache NetBeans IDE.  You can run the IDE by invoking the incubator-netbeans/nbbuild/netbeans/bin/netbeans executable.


Once the executable is started, you will be presented with the new Apache NetBeans 9.0 splash screen, and you can then begin to use the latest build.



Monday, May 21, 2018

Apache NetBeans 9.0 - Release Candidate Review



First, I want to thank all of the developers that have been working hard on the Apache NetBeans 9.0 release.  I am on the mailing lists and these developers have been working around the clock to ensure that this release is solid.  I was part of the NetBeans 9.0 NETCAT testing team for the profiler tool this year.  I only contributed a few hours of testing, but it was a great experience to be part of the team helping to move NetBeans forward.  There are plenty of people who have contributed dozens of hours...so thank you!

Release Candidate 1 (currently in testing and not yet officially released) can be downloaded using the following links:

Build Artifacts:
https://dist.apache.org/repos/dist/dev/incubator/netbeans/incubating-netbeans-java/incubating-9.0-rc1-rc1

ZIP Artifact:
https://dist.apache.org/repos/dist/dev/incubator/netbeans/incubating-netbeans-java/incubating-9.0-rc1-rc1/incubating-netbeans-java-9.0-rc1-source.zip

This release of NetBeans constitutes all of the modules within the Apache NetBeans GitHub repository ((https://github.com/apache/incubator-netbeans).  These modules include the NetBeans Platform and the full IDE for Java SE development.

There are still a number of tasks that will need to be performed before moving forward, but Release Candidate 1 is a huge step.  In the coming days/weeks, there will likely be another release candidate which will include some final pieces, such as a new splash screen.  We are getting very close to the official Apache NetBeans IDE 9.0 release.

Keep in mind that this first release does not include the transfer of all NetBeans modules.  For instance, the Java EE modules have not yet been transferred or included in Apache NetBeans 9.0.  That transfer will occur sometime in the future.  However, it is still possible to develop Java EE applications using Apache NetBeans 9.0 by simply installing the Java EE related NetBeans 8.2 plugins.  I am doing this today and it works well!

Stay tuned during the coming weeks for the official release of Apache NetBeans 9.0.  In the meantime, get involved!  Please download the Release Candidate and take it for a spin.  Join the NetBeans developer mailing list to provide feedback and/or vote on upcoming release candidates.

https://netbeans.apache.org/