Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
Spring Music
============

This is a sample application for using database services on [Cloud Foundry](http://cloudfoundry.org) with the [Spring Framework](http://spring.io) and [Spring Boot](http://projects.spring.io/spring-boot/).
This is a sample application for using database services on [Cloud Foundry](https://www.cloudfoundry.org) with the [Spring Framework](https://spring.io) and [Spring Boot](https://spring.io/projects/spring-boot).

This application has been built to store the same domain objects in one of a variety of different persistence technologies - relational, document, and key-value stores. This is not meant to represent a realistic use case for these technologies, since you would typically choose the one most applicable to the type of data you need to store, but it is useful for testing and experimenting with different types of services on Cloud Foundry.

The application use Spring Java configuration and [bean profiles](http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html) to configure the application and the connection objects needed to use the persistence stores. It also uses the [Java CFEnv](https://github.com/pivotal-cf/java-cfenv/) library to inspect the environment when running on Cloud Foundry. See the [Cloud Foundry documentation](http://docs.cloudfoundry.org/buildpacks/java/spring-service-bindings.html) for details on configuring a Spring application for Cloud Foundry.
The application uses Spring Java configuration and [bean profiles](https://docs.spring.io/spring-boot/reference/features/profiles.html) to configure the application and the connection objects needed to use the persistence stores. It also uses the [Java CFEnv](https://github.com/pivotal-cf/java-cfenv/) library to inspect the environment when running on Cloud Foundry.

## Tech stack

| Component | Version |
|-----------|---------|
| Java | 25 |
| Spring Boot | 4.0.3 |
| Gradle | 9.3.1 |
| Java CFEnv | 4.0.0 |

## Building

This project requires Java version 17 or later to compile.
This project requires Java 25 or later to compile.

> [!NOTE]
> If you need to use an earlier Java version, check out the [`spring-boot-2` branch](https://github.com/cloudfoundry-samples/spring-music/tree/spring-boot-2), which can be built with Java 8 and later.
> If you need to use an earlier Java version, check out the [`spring-boot-2` branch](https://github.com/cloudfoundry-samples/spring-music/tree/spring-boot-2), which can be built with Java 8 and later.

To build a runnable Spring Boot jar file, run the following command:

Expand All @@ -37,7 +46,7 @@ where `<profile>` is one of the following values:
* `mongodb`
* `redis`

If no profile is provided, an in-memory relational database will be used. If any other profile is provided, the appropriate database server must be started separately. Spring Boot will auto-configure a connection to the database using it's auto-configuration defaults. The connection parameters can be configured by setting the appropriate [Spring Boot properties](http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html).
If no profile is provided, an in-memory H2 relational database will be used. If any other profile is provided, the appropriate database server must be started separately. Spring Boot will auto-configure a connection to the database using its auto-configuration defaults. The connection parameters can be configured by setting the appropriate [Spring Boot properties](https://docs.spring.io/spring-boot/appendix/application-properties/index.html).

If more than one of these profiles is provided, the application will throw an exception and fail to start.

Expand All @@ -49,7 +58,7 @@ If no bound services are found containing any of these values in the name, an in

If more than one service containing any of these values is bound to the application, the application will throw an exception and fail to start.

After installing the 'cf' [command-line interface for Cloud Foundry](http://docs.cloudfoundry.org/cf-cli/), targeting a Cloud Foundry instance, and logging in, the application can be built and pushed using these commands:
After installing the `cf` [command-line interface for Cloud Foundry](https://docs.cloudfoundry.org/cf-cli/), targeting a Cloud Foundry instance, and logging in, the application can be built and pushed using these commands:

~~~
$ cf push
Expand All @@ -59,7 +68,7 @@ The application will be pushed using settings in the provided `manifest.yml` fil

### Creating and binding services

Using the provided manifest, the application will be created without an external database (in the `in-memory` profile). You can create and bind database services to the application using the information below.
Using the provided manifest, the application will be created without an external database (using the in-memory H2 profile). You can create and bind database services to the application using the information below.

#### System-managed services

Expand Down Expand Up @@ -107,26 +116,32 @@ $ cf restart

Database drivers for MySQL, Postgres, Microsoft SQL Server, MongoDB, and Redis are included in the project.

To connect to an Oracle database, you will need to download the appropriate driver (e.g. from http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html). Then make a `libs` directory in the `spring-music` project, and move the driver, `ojdbc7.jar` or `ojdbc8.jar`, into the `libs` directory.
In `build.gradle`, uncomment the line `compile files('libs/ojdbc8.jar')` or `compile files('libs/ojdbc7.jar')` and run `./gradle assemble`.
To connect to an Oracle database, you will need to download the appropriate driver from the [Oracle JDBC Downloads page](https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html). Then make a `libs` directory in the `spring-music` project, and move the driver (`ojdbc8.jar` or `ojdbc7.jar`) into the `libs` directory.
In `build.gradle`, uncomment the appropriate line:

~~~
implementation files('libs/ojdbc8.jar')
// or
implementation files('libs/ojdbc7.jar')
~~~

Then run `./gradlew assemble`.

## Alternate Java versions

By default, the application will be built and deployed using Java 17 compatibility.
If you want to use a more recent version of Java, you will need to update two things.
By default, the application is built and deployed using Java 25. To use a different version, update two places.

In `build.gradle`, change the `targetCompatibility` Java version from `JavaVersion.VERSION_17` to a different value from `JavaVersion`:
In `build.gradle`, change the `targetCompatibility` version:

~~~
java {
...
targetCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_25
}
~~~

In `manifest.yml`, change the Java buildpack JRE version from `version: 17.+` to a different value:
In `manifest.yml`, change the Java buildpack JRE version:

~~~
JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 17.+ } }'
JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 25.+ } }'
~~~
30 changes: 16 additions & 14 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
id 'org.springframework.boot' version '4.0.3'
id 'java'
id 'eclipse-wtp'
id 'idea'
Expand All @@ -9,15 +8,15 @@ plugins {
group = 'org.cloudfoundry.samples.music'

java {
sourceCompatibility = '17'
sourceCompatibility = '25'
}

repositories {
mavenCentral()
}

ext {
javaCfEnvVersion = '3.1.2'
javaCfEnvVersion = '4.0.0'
}

dependencies {
Expand All @@ -43,27 +42,26 @@ dependencies {
// runtime "io.lettuce:lettuce-core"

// Webjars
implementation "org.webjars:bootstrap:3.1.1"
implementation "org.webjars:angularjs:1.2.16"
implementation "org.webjars:angular-ui:0.4.0-2"
implementation "org.webjars:angular-ui-bootstrap:0.10.0-1"
implementation "org.webjars:jquery:2.1.0-2"
implementation "org.webjars:bootstrap:3.4.1"
implementation "org.webjars:angularjs:1.8.3"
implementation "org.webjars:angular-ui:0.4.0-3"
implementation "org.webjars:angular-ui-bootstrap:0.14.3"
implementation "org.webjars:jquery:3.7.1"

// Oracle - uncomment one of the following after placing driver in ./libs
// compile files('libs/ojdbc8.jar')
// compile files('libs/ojdbc7.jar')

// Testing
testImplementation "junit:junit"
testImplementation "org.springframework.boot:spring-boot-starter-test"
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25

if (JavaVersion.current() != project.targetCompatibility) {
logger.warn("The build is using Java ${JavaVersion.current()} to build a Java ${project.targetCompatibility} compatible archive.")
if (JavaVersion.current() != JavaVersion.VERSION_25) {
logger.warn("The build is using Java ${JavaVersion.current()} to build a Java ${JavaVersion.VERSION_25} compatible archive.")
logger.warn("See the project README for instructions on changing the target Java version.")
}
}
Expand All @@ -72,6 +70,10 @@ jar {
enabled = false
}

test {
useJUnitPlatform()
}

tasks.named("bootBuildImage") {
builder = "paketobuildpacks/builder-jammy-base:latest"
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
4 changes: 3 additions & 1 deletion manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ applications:
memory: 1G
random-route: true
path: build/libs/spring-music-1.0.jar
buildpacks:
- java_buildpack_offline
env:
JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}'
SPRING_PROFILES_ACTIVE: http2
JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 17.+ } }'
JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 25.+ } }'
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import io.pivotal.cfenv.core.CfEnv;
import io.pivotal.cfenv.core.CfService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration;
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoRepositoriesAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.DataRedisRepositoriesAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
Expand All @@ -20,8 +20,6 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -30,17 +28,16 @@

public class SpringApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

private static final Log logger = LogFactory.getLog(SpringApplicationContextInitializer.class);
private static final Logger logger = LoggerFactory.getLogger(SpringApplicationContextInitializer.class);

private static final Map<String, List<String>> profileNameToServiceTags = new HashMap<>();
static {
profileNameToServiceTags.put("mongodb", Collections.singletonList("mongodb"));
profileNameToServiceTags.put("postgres", Collections.singletonList("postgres"));
profileNameToServiceTags.put("mysql", Collections.singletonList("mysql"));
profileNameToServiceTags.put("redis", Collections.singletonList("redis"));
profileNameToServiceTags.put("oracle", Collections.singletonList("oracle"));
profileNameToServiceTags.put("sqlserver", Collections.singletonList("sqlserver"));
}
private static final Map<String, List<String>> profileNameToServiceTags = Map.of(
"mongodb", List.of("mongodb"),
"postgres", List.of("postgres"),
"mysql", List.of("mysql"),
"redis", List.of("redis"),
"oracle", List.of("oracle"),
"sqlserver", List.of("sqlserver")
);

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
Expand Down Expand Up @@ -115,7 +112,7 @@ private void excludeAutoConfiguration(ConfigurableEnvironment environment) {
excludeRedisAutoConfiguration(exclude);
}

Map<String, Object> properties = Collections.singletonMap("spring.autoconfigure.exclude",
Map<String, Object> properties = Map.of("spring.autoconfigure.exclude",
StringUtils.collectionToCommaDelimitedString(exclude));

PropertySource<?> propertySource = new MapPropertySource("springMusicAutoConfig", properties);
Expand All @@ -130,15 +127,15 @@ private void excludeDataSourceAutoConfiguration(List<String> exclude) {
private void excludeMongoAutoConfiguration(List<String> exclude) {
exclude.addAll(Arrays.asList(
MongoAutoConfiguration.class.getName(),
MongoDataAutoConfiguration.class.getName(),
MongoRepositoriesAutoConfiguration.class.getName()
DataMongoAutoConfiguration.class.getName(),
DataMongoRepositoriesAutoConfiguration.class.getName()
));
}

private void excludeRedisAutoConfiguration(List<String> exclude) {
exclude.addAll(Arrays.asList(
RedisAutoConfiguration.class.getName(),
RedisRepositoriesAutoConfiguration.class.getName()
DataRedisAutoConfiguration.class.getName(),
DataRedisRepositoriesAutoConfiguration.class.getName()
));
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.cloudfoundry.samples.music.config.data;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import org.cloudfoundry.samples.music.domain.Album;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
Expand All @@ -21,7 +23,7 @@ public RedisTemplate<String, Album> redisTemplate(RedisConnectionFactory redisCo
template.setConnectionFactory(redisConnectionFactory);

RedisSerializer<String> stringSerializer = new StringRedisSerializer();
RedisSerializer<Album> albumSerializer = new Jackson2JsonRedisSerializer<>(Album.class);
RedisSerializer<Album> albumSerializer = albumSerializer();

template.setKeySerializer(stringSerializer);
template.setValueSerializer(albumSerializer);
Expand All @@ -31,4 +33,28 @@ public RedisTemplate<String, Album> redisTemplate(RedisConnectionFactory redisCo
return template;
}

private RedisSerializer<Album> albumSerializer() {
ObjectMapper mapper = JsonMapper.builder().build();
return new RedisSerializer<Album>() {
@Override
public byte[] serialize(Album album) throws SerializationException {
try {
return mapper.writeValueAsBytes(album);
} catch (Exception e) {
throw new SerializationException("Could not serialize Album", e);
}
}

@Override
public Album deserialize(byte[] bytes) throws SerializationException {
if (bytes == null) return null;
try {
return mapper.readValue(bytes, Album.class);
} catch (Exception e) {
throw new SerializationException("Could not deserialize Album", e);
}
}
};
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package org.cloudfoundry.samples.music.domain;

import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UuidGenerator;

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

@Entity
public class Album {
@Id
@Column(length=40)
@GeneratedValue(generator="randomId")
@GenericGenerator(name="randomId", strategy="org.cloudfoundry.samples.music.domain.RandomIdGenerator")
@UuidGenerator
private String id;

private String title;
Expand Down
Loading