Friday, April 14, 2023

Spring Boot Beginning

Spring Boot Beginning

Using VSCode Dev Containers for Java Development

  1. Open VSCode, press F1 or Ctrl+Shift+P, type Dev Containers: Open Folder in containers..., and select your project directory.
  2. Choose Java as the Dev Container image and wait for the container to start.
  3. Open TERMINAL, and verify Java installation with the following commands:
Copy code
$ java -version
$ javac -version

In the Linux-based Dev Container, check the OS version and Java installation path:

Copy code
# Check OS
cat /etc/os-release

# Find Java installation
which javac

Set up GitLab Repository

  1. Create a new blank project on GitLab.
  2. Execute these commands to push your local data to GitLab:
Copy code
git init
git remote add origin https://gitlab.com/<group>/<project>.git
git add .
git commit -m "Initial commit"
git push -u origin master

Create a Java Spring Boot Project

  1. Install the “Spring Initializr Java Support” extension in VS Code.
  2. Press Ctrl+Shift+X, search for the extension, and click Install in Dev Container: Java.
  3. Open Command Palette (Ctrl+Shift+P or F1) and type “spring”. Choose Maven or Gradle to create the project.

This tutorial uses a Spring Boot API project managed by Maven. Select Spring Web, Spring Boot DevTools, and Spring Data as dependencies. Check Maven installation with mvn -v.

Create a package with Maven:

mvn package

Run the generated JAR file (e.g., backend-api-0.0.1-SNAPSHOT.jar):

java -jar ./target/backend-api-0.0.1-SNAPSHOT.jar

During development, use this command to compile and restart automatically after code changes:

mvn spring-boot:run

Implement OpenAPI and Swagger UI

To display Swagger UI with Spring Boot 3.0.0, use springdoc-openapi v2.1.0. Refer to the official springdoc-openapi v2.1.0 documentation. Add the following text to pom.xml:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.1.0</version>
</dependency>

The Swagger UI page can be accessed at http://server:port/context-path/swagger-ui.html, and the OpenAPI Spec (JSON) is located at http://server:port/context-path/v3/api-docs.

To generate only the OpenAPI document (JSON) without the UI, include this dependency:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
  <version>2.1.0</version>
</dependency>

For Spring Boot 2.x versions, use the following package:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-ui</artifactId>
  <version>1.7.0</version>
</dependency>

The paths for Swagger UI and OpenAPI Spec remain the same.

Spring Boot Layerd Architecture

Recommended source code directory structure:

src/
├── main/
│   ├── java/
│   │   ├── beginning/
│   │   │   ├── example/
|   │   │   │   ├── backendapi/
|   │   │   │   │   ├── filters/
|   │   │   │   │   ├── configs/
|   │   │   │   │   ├── controllers/
|   │   │   │   │   ├── services/
|   │   │   │   │   ├── repositories/
|   │   │   │   │   └── models/
|   │   │   │   └── BackendApiApplication.java
|   │   │   └── resources/
|   │   │       └── application.properties
|   │   └── ...
│   └── ...
└── ...
  • controllers: Receive HTTP requests and respond with HTTP responses.
  • models: Define data structures for transfer and processing.
  • services: Handle data processing and business logic.
  • repositories: Connect to backend databases for data access.
  • filters: Middleware with OncePerRequestFilter as the base class, enabling tasks such as logging or security checks on HTTP requests and responses.
  • configs: Implement WebMvcConfigurer to add filters to the appropriate locations.

Controllers

Controllers simplify the development of RESTful web services. By using the @RestController annotation on a class, it can handle various HTTP requests such as GET, POST, and others.

Services

Services handle business logic and act as intermediaries for other layers (e.g., controllers). By annotating a class with @Service, Spring framework automatically creates an instance at startup. Controllers can declare a service variable using @Autowired to automatically obtain its instance. If multiple services exist, use @Qualifier to specify one.

For example, if the program contains userService1 and userService2:

@Service("userService1")
public class UserService1Impl implements UserService {
    // ...
}

@Service("userService2")
public class UserService2Impl implements UserService {
    // ...
}

If the controller selects one of the services, use the following pattern:

@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(@Qualifier("userService1") UserService userService) {
        this.userService = userService;
    }

    // ...
}

// You can also set @Qualifier directly on the service variable without using a constructor for more concise code.
@RestController
public class UserController {
    @Autowired
    @Qualifier("userService1")
    private UserService userService;

    // ...
}

If you want to use both services, use the following pattern:

@RestController
public class UserController {
    private final UserService userService1;
    private final UserService userService2;

    @Autowired
    public UserController(@Qualifier("userService1") UserService userService1, @Qualifier("userService2") UserService userService2) {
        this.userService1 = userService1;
        this.userService2 = userService2;
    }

    // ...
}

Repositories

Repositories are responsible for handling data access, such as connecting to databases and performing read and write operations.

Models

Models define the object form of data, such as the data required for HTTP requests and responses, or the data formats needed for internal conversions. To map a model directly to an SQL type database using JPA, annotate with @Entity, @Table, and @Column as shown below:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    // getter and setter methods...
}

Filters

Filters in Spring Boot act as middleware for HTTP requests, intercepting HTTP request and response information for processing, such as adding logs, performing security checks, and handling encryption/decryption tasks.

To create a filter, simply extend OncePerRequestFilter and override the doFilterInternal method.

After completing the filter code, it needs to be registered with Spring using the configure method.

@Configuration

Use @Configuration to define the filter as a @Bean for injection into the Dependency Injection Container. Spring Boot searches for the Filter interface in the DI Container and calls it when receiving an HTTP request. The Filter interface is defined as follows:

package javax.servlet;
import java.io.IOException;

public interface Filter {
    void init(FilterConfig filterConfig) throws ServletException;
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
    void destroy();
}

Configure File: application.properties

After executing mvn package, the application.properties file is copied to the target/classes directory. application.properties can be used to store data that varies depending on the installation environment, such as database connection locations.

Dockerfile

Create a Docker Image:

docker build -t try-spring-boot .

When executed repeatedly, the old try-spring-boot image is not deleted but is renamed to <none>. Use the following commands to delete these unused <none> images:

docker image prune -f
# Or chain with the previous build command
docker build -t try-spring-boot . && docker image prune -f

Run the container:

docker run -d --name try-spring-boot -p 8080:8080 try-spring-boot

To view the logs of a running container, use the following commands:

docker logs try-spring-boot

# Or continuously output logs until Ctrl-C is pressed
docker logs try-spring-boot -f

Stop the container:

docker stop try-spring-boot

Open the web pages:

Connecting to MongoDB

First, we can try running two containers, one for MongoDB and the other for mongosh. We can connect and access data through the assigned Docker network. The following example will create and enter the test database using the mongo shell.

docker network create try-spring-boot-network
docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d mongo
docker run --name try-spring-boot-mongo-sh --rm --network try-spring-boot-network -it mongo mongosh --host try-spring-boot-mongo test

In the above commands, the second command sets the host name to the container name by default when the --hostname parameter is not specified. Therefore, the shell can connect to MongoDB through this host name. However, to connect to the container from the local machine, you still need to use localhost. To exit the mongo shell, enter .exit.

If you want to set the username and password through environment variables, you can use the following commands:

docker network create try-spring-boot-network

docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=example mongo

docker run --name try-spring-boot-mongo-sh --rm --network try-spring-boot-network -it mongo /bin/bash

# After entering try-spring-boot-mongo-sh, enter the following command to start mongosh
mongosh --host try-spring-boot-mongo --username root --password example --authenticationDatabase admin

To connect to the above-created mongo in the dev container, you need to specify the same network in ./devcontainer/devcontainer.json.

{
	"name": "Java",
	"image": "mcr.microsoft.com/devcontainers/java:0-17",
  // Add the dev container to the try-spring-boot-network
	"runArgs": [
		"--network=try-spring-boot-network"
	],

	"features": {
		"ghcr.io/devcontainers/features/java:1": {
			"version": "none",
			"installMaven": "true",
			"installGradle": "false"
		}
	}
}

Also, modify application.properties and vscode launch.json to configure the connection.

spring.data.mongodb.uri=mongodb://${MONGO_USERNAME:root}:${MONGO_PASSWORD:example}@${MONGO_HOST:mongo}:${MONGO_PORT:27017}/${MONGO_DATABASE:test}?authSource=${MONGO_AUTH_SOURCE:admin}&authMechanism=${MONGO_AUTHMECHANISM:SCRAM-SHA-1}
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "BackendApiApplication",
            "request": "launch",
            "mainClass": "beginning.example.backendapi.BackendApiApplication",
            "projectName": "backend-api",
            "env": {
                "MONGO_USERNAME": "root",
                "MONGO_PASSWORD": "example",
                "MONGO_HOST": "try-spring-boot-mongo",
                "MONGO_PORT": "27017",
                "MONGO_DATABASE": "test",
                "MONGO_AUTH_SOURCE": "admin",
                "MONGO_AUTHMECHANISM": "SCRAM-SHA-1"
            }
        }
    ]
}

Using docker-compose is a more convenient choice for running multiple containers simultaneously. The following method can be used to start the Java program as well.

version: '3.1'

services:

  mongo:
    image: mongo
    container_name: try-spring-boot-mongo
    restart: always
    volumes:
      - ${PWD}/temp-db-storage:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    networks:
      - try-spring-boot-network

  mongo-express:
    image: mongo-express
    container_name: try-spring-boot-mongo-express
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
      ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
    networks:
      - try-spring-boot-network

  try-spring-boot:
    image: try-spring-boot
    container_name: try-spring-boot
    ports:
      - "8080:8080"
    environment:
      MONGO_USERNAME: root,
      MONGO_PASSWORD": example,
      MONGO_HOST: try-spring-boot-mongo,
      MONGO_PORT: 27017,
      MONGO_DATABASE: test,
      MONGO_AUTH_SOURCE: admin,
      MONGO_AUTHMECHANISM: SCRAM-SHA-1
    networks:
      - try-spring-boot-network      

networks:
  try-spring-boot-network:
    driver: bridge

The above yaml can be started or stopped using the following commands:

docker-compose up -d
docker-compose down

By using the provided docker-compose.yml, you can run MongoDB, Mongo Express, and your Spring Boot application simultaneously. This setup simplifies managing and connecting multiple containers, making development and testing more efficient.

Remember to modify your Spring Boot application.properties and your VSCode launch.json accordingly to ensure proper connection to the MongoDB instance.

Once everything is set up, you can test your Spring Boot application’s connection to MongoDB, perform database operations, and build your application as needed.

Setting Up a DevContainer Debug Environment

To set up a DevContainer testing environment for debugging within DevContainer, you can refer to the PowerShell script setup-mongo-container.ps1 located in the root directory of the project. Also, make sure to configure the env information in the .vscode launch.json file accordingly.

The contents of setup-mongo-container.ps1 are as follows:

# 1. Check if the directory does not exist, then create it
if (!(Test-Path -Path .\temp-db-storage)) {
    New-Item -ItemType Directory -Path .\temp-db-storage
}

# 2. Check if the try-spring-boot-network does not exist, then create it
$networkExists = docker network ls --filter name=try-spring-boot-network --format "{{.Name}}" -q
if (!$networkExists) {
    docker network create try-spring-boot-network
}

# 3. Check if the container exists, stop and remove it if it does, then run the new container
$containerExists = docker container ls -a --filter name=try-spring-boot-mongo --format "{{.Names}}" -q
if ($containerExists) {
    docker container stop try-spring-boot-mongo
    docker container rm try-spring-boot-mongo
}

docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=example mongo

This PowerShell script performs the following steps:

  1. Creates a temp-db-storage directory if it doesn’t already exist.
  2. Creates a try-spring-boot-network Docker network if it doesn’t already exist.
  3. Checks for the existence of a try-spring-boot-mongo container. If it exists, the script stops and removes it before running a new container with the same name.

By executing this script, you can easily set up the testing environment for debugging within DevContainer. This will allow you to create and manage the necessary resources (such as the Docker network and container) for your Spring Boot and MongoDB integration.

Once you’ve set up the testing environment, you can proceed with debugging and developing your application inside the DevContainer environment.

Deploying Vue & .NET with Google OAuth on GCP Cloud Run

Deploying Vue & .NET with Google OAuth on GCP Cloud Run Deploying Vue & .NET with Google OAuth on GCP Cloud Run...