Spring Example with VS Code: Difference between revisions
Line 335: | Line 335: | ||
return pwsPeriodAttendanceService | return pwsPeriodAttendanceService | ||
.getPeriodAttendancesByPeriods(new BaseRequest(serviceType, startDate, endDate, person)); | .getPeriodAttendancesByPeriods(new BaseRequest(serviceType, startDate, endDate, person)); | ||
} | |||
</syntaxhighlight> | |||
=Union Classes= | |||
In typescript you can have something like this which allows multiple types to be used | |||
<syntaxhighlight lang="ts"> | |||
type MyUnionType = String | Number | MyType | |||
</syntaxhighlight> | |||
For Java we use the permits | |||
<syntaxhighlight lang="java"> | |||
package local.bibble.brat.types; | |||
public sealed interface NonAttendance permits NonAttendanceValue, NonAttendanceNumber { | |||
// Override toString method | |||
@Override | |||
public String toString(); | |||
} | |||
</syntaxhighlight> | |||
Now we can use either type and instanceof | |||
<syntaxhighlight lang="java"> | |||
public static NonAttendanceValue getOperatorAsValue(NonAttendance operator, int reasonId, Double percentage) { | |||
return operator instanceof NonAttendanceValue ? (NonAttendanceValue) operator | |||
: new NonAttendanceValue(percentage, reasonId, 0, 0); | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 04:30, 23 October 2024
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;
Optional Parameters on HTTP Request
To use optional parameters I used the, Optional, type in Java. The optional type means you can use get, to get the value and isPresent() to determine if there. Seemed similar to Rust
@GetMapping
public Attendance[] getAttendanceByPeriod(
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate,
@RequestParam("person") Optional<String> person) {
return pwsPeriodAttendanceService
.getPeriodAttendancesByPeriods(new BaseRequest(serviceType, startDate, endDate, person));
}
Union Classes
In typescript you can have something like this which allows multiple types to be used
type MyUnionType = String | Number | MyType
For Java we use the permits
package local.bibble.brat.types;
public sealed interface NonAttendance permits NonAttendanceValue, NonAttendanceNumber {
// Override toString method
@Override
public String toString();
}
Now we can use either type and instanceof
public static NonAttendanceValue getOperatorAsValue(NonAttendance operator, int reasonId, Double percentage) {
return operator instanceof NonAttendanceValue ? (NonAttendanceValue) operator
: new NonAttendanceValue(percentage, reasonId, 0, 0);
}
Serializing
Been a while since some of Java but another thing I wanted to do was change the behavior of the serializer. To do this is real easy but here to remind someone as old as I am what to do. You just write a custom serializer and use it
public class NonAttendanceNumberSerializer extends StdSerializer<NonAttendanceNumber> {
public NonAttendanceNumberSerializer() {
this(null);
}
public NonAttendanceNumberSerializer(Class<NonAttendanceNumber> t) {
super(t);
}
@Override
public void serialize(
NonAttendanceNumber value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeRawValue(value.toString());
}
}
And to use
@Getter
@JsonSerialize(using = NonAttendanceNumberSerializer.class)
public final class NonAttendanceNumber implements NonAttendance {
Long value;
public NonAttendanceNumber(Long value) {
this.value = value;
}
@Override
public String toString() {
return value.toString();
}
}