Spring Example with VS Code

From bibbleWiki
Jump to navigation Jump to search

Introduction

To make sure my Java is keeping up with my Typescript I revisited Spring to see what it would take to build a REST API using the Spring Web framework. The goal was

  • Create a REST API
  • Create GET endpoints
  • Use a database in Spring
  • Add Filtering to endpoint
  • Add OAuth to endpoint

Installation

This was remarkably easy.

Install Java

sudo apt install default-jdk -y

Install Maven

We grab lastest and put it in /opt

  wget https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz
  sudo tar xf apache-maven-3.9.9-bin.tar.gz -C /opt

Make a profile in /etc/profile.d/maven.sh

export JAVA_HOME=/usr/lib/jvm/default-java 
export M3_HOME=/opt/apache-maven-3.9.9
export MAVEN_HOME=/opt/apache-maven-3.9.9
export PATH=${M3_HOME}/bin:${PATH}

Now we can have an environment with . /etc/profile.d/maven.sh

VS Code Setup

For me I installed

  • Java Extension Pack
  • Spring Boot Extension Pack
  • Spring Initializer

Create New Project

You can do this using the Spring Initializer extension. For this project we need

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa

For me the main change was to rename application.properties to application.yml so I could configure the project better. This included fixing the port and and the request header size

spring:
  application:
    name: springbibble
server:
  port: 8082
  max-http-request-header-size: 10MB

Making an Endpoint

This again could not be easier. All I had to do was in the Java Project explorer, press plus against my application and it prompted me to add a class. Type in pingController and we were away. Just add annotations to controller and endpoint

@RestController
public class pingController {

    @GetMapping("/ping")
    public String ping() {
        return "pong";
    }
}

Adding Snowflake

My last place used snowflake and wasn't keen but this is odd enough to demonstrate how to do it for anything. In pom.xml, add the dependency.

		<dependency>
			<groupId>net.snowflake</groupId>
    		<artifactId>snowflake-jdbc</artifactId>
    		<version>3.13.34</version>
		</dependency>

Then we need to specify the credential in the application.yml

spring:
  application:
    name: abrat

  datasource:
    username: ${SNOWFLAKE_USERNAME}
    password: ${SNOWFLAKE_PASSWORD}
    driverClassName: net.snowflake.client.jdbc.SnowflakeDriver
    url: jdbc:snowflake://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/?db=${SNOWFLAKE_DATABASE}&schema=${SNOWFLAKE_SCHEMA}&warehouse=${SNOWFLAKE_WAREHOUSE}&role=${SNOWFLAKE_ROLE}

Making a proper endpoint

To model my approach used in Typescript we will have

  • Controller
  • Service
  • Repository
  • Entity

None of these are difficult and with Spring a lot of the code comes for fred

Controller

package local.bibble.brat.controllers;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import local.bibble.brat.entities.Area;
import local.bibble.brat.services.AreaService;

@RestController
@RequestMapping("/api/areas")
public class areaController {

    @Autowired
    private AreaService areaService;

    @GetMapping
    public List<Area> getLocationId(@RequestParam("id") Long id) {
        return areaService.findByLocationId(id);
    }

}

Service

package local.bibble.brat.services;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import local.bibble.brat.entities.Area;
import local.bibble.brat.repositories.AreaRepository;

@Service
public class AreaService {

    @Autowired
    AreaRepository areaRepository;

    public List<Area> findByLocationId(Long id) {
        return areaRepository.getOneByLocationId(id);
    }
}

Repository

package local.bibble.brat.repositories;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import local.bibble.brat.entities.Area;

public interface AreaRepository extends JpaRepository<Area, Long> {
    public List<Area> getOneByLocationId(Long id);
}

Entity

package local.bibble.brat.entities;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "LOCATION")
public class Area {
    @Id
    @Column(name = "TLOCATION_ID")
    private Long locationId;

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

    public Long getLocationId() {
        return locationId;
    }

    public String getLocationName() {
        return locationName;
    }
}

Problem 1

So getting this to work should have been straight forward but getting back into Maven, Pom and java meant getting something up and running was 4 hours instead of one. The problem was how to pass options to Maven as the JDBC driver was broken.

_JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" mvn spring-boot:run

Setting Java Options

The _JAVA_OPTIONS (note the underscore) is what you use to set the value outside of running maven. Inside you can set the arguments passed to the jvm in the pom.xml using the jvmArguments

...
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
    				<jvmArguments>--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED</jvmArguments>
  				</configuration>
			</plugin>
		</plugins>
	</build>

Running the example

We need to

  • initialize maven
  • set up environment variable
  • set _JAVA_OPTIONS
. /etc/profile.d/maven.sh
. .env.local
# If you haven't set this in the pom
# _JAVA_OPTIONS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" mvn spring-boot:run

Adding OAuth2 with a Bear Token

This this validating a JWT set up in Azure using an Application Registration. This was remarkably easy to do. You just need to

  • Add Package
  • Add Application settings
  • Add Security Configuration

Add Package

A lot of the examples use WebSecurityConfigurerAdapter but this has been superseded in Spring Security 5.7 [here]

     	<dependency>
      		<groupId>org.springframework.boot</groupId>
      		<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    	</dependency>

Add Application Settings

Nothing special here

  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithms: RS256
          jwk-set-uri: https://login.microsoftonline.com/${API_TENANT_ID}/discovery/v2.0/keys
          issuer-uri: https://sts.windows.net/${API_TENANT_ID}/

Add Securing Configuration

I put mine under security/configuration but it can be anywhere of course. Wasn't happy with my syntax as a new Lambda DSL syntax is available.

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
/* 
 * Old syntax and lots of old methods
 * And add ping as public 
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt()
                .and();
*/
        http.authorizeHttpRequests(requests -> requests
                .requestMatchers("/ping").permitAll()
                .anyRequest().authenticated())
                .oauth2ResourceServer(server -> server
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }
}

Fun with Snowflake

It hates me, and I hate it but snowflake cost me a bit of free time. I wanted to write my own query to select the data. It looked easy and suggested you just add @query on the repository and good to go. But oh no. In fairness the first problem has hibernate. Here is my first attempt

    @Query(value = """
            SELECT
               id
            FROM
               LOGS_NON_VIEW
            WHERE
               id = :id
             """)
public List<PWSAttendance> getOneByServiceLogId(Long tservice_log_id);

This let me to creating a named query in the entity which clearly is a bad thing as the entity has nothing to do with database - it just where the result is stored.

@NamedNativeQuery(name = "FRED", """
            SELECT
               id
            FROM
               LOGS_NON_VIEW
            WHERE
               id = :id
             """, resultClass = PWSAttendance.class)

This did not work either and complained about not finding the column. Which led me to changing both the entity and the repository to use a mixture of upper and lower case in a vain attempt to get it to work. I also looked at physical_naming_strategy in hibernate but it still kept saying column not found. Eventually I found the answer we snowflake (of course) and the JDBC driver. I never thought 30 years on I would be messing with JDBC drivers. So in the application.yaml you need to set CLIENT_RESULT_COLUMN_CASE_INSENSITIVE=true

datasource:
    username: ${SNOWFLAKE_USERNAME}
    password: ${SNOWFLAKE_PASSWORD}
    driverClassName: net.snowflake.client.jdbc.SnowflakeDriver
    url: jdbc:snowflake://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/?db=${SNOWFLAKE_DATABASE}&schema=${SNOWFLAKE_SCHEMA}&warehouse=${SNOWFLAKE_WAREHOUSE}&role=${SNOWFLAKE_ROLE}&CLIENT_RESULT_COLUMN_CASE_INSENSITIVE=true

Not we have fixed the root course we can fix it properly. I moved the query back to the repository.

    @Query(value = """
            SELECT
               id
            FROM
               LOGS_NON_VIEW
            WHERE
               id = :id
            """, nativeQuery = true)
    // @Query(name = "FRED")
    public List<PWSAttendance> getOneByServiceLogId(Long tservice_log_id);

We can now make an entity

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;

@Getter
@Entity(name = "ASSIGNED_SERVICE")
public class PWSAttendance {

    @Id
    @Column(name = "ID")
    private Long id;