Feature Flag Service check-in

This commit is contained in:
Apaar Jain 2026-02-08 18:02:36 +05:30
parent ab1d5364a0
commit a80ab1b3ba
14 changed files with 497 additions and 113 deletions

241
README.md
View file

@ -1,20 +1,30 @@
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml)
# Feature Flag System - Spring PetClinic Integration
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918)
## Understanding the Spring Petclinic application with a few diagrams
See the presentation here:
[Spring Petclinic Sample Application (legacy slides)](https://speakerdeck.com/michaelisvy/spring-petclinic-sample-application?slide=20)
> **Note:** These slides refer to a legacy, preSpring Boot version of Petclinic and may not reflect the current Spring Bootbased implementation.
> For up-to-date information, please refer to this repository and its documentation.
## Overview
This project adds a custom-built Feature Flag system to the Spring PetClinic application without using any thirdparty feature flag libraries. The system allows runtime control of selected features using database-driven flags and an AOP-based custom annotation.
## Run Petclinic locally
Features can be enabled/disabled globally and also support whitelist, blacklist, and percentage rollout strategies.
Spring Petclinic is a [Spring Boot](https://spring.io/guides/gs/spring-boot) application built using [Maven](https://spring.io/guides/gs/maven/) or [Gradle](https://spring.io/guides/gs/gradle/).
Java 17 or later is required for the build, and the application can run with Java 17 or newer.
---
## Tech Stack
- Java 17+
- Spring Boot 4.0.1 (Default version of Spring PetClinic)
- Spring AOP
- Spring Data JPA
- MySQL
- Maven
---
## How To Run Locally
### Clone Repo
You first need to clone the project locally:
@ -22,153 +32,158 @@ You first need to clone the project locally:
git clone https://github.com/spring-projects/spring-petclinic.git
cd spring-petclinic
```
If you are using Maven, you can start the application on the command-line as follows:
```bash
./mvnw spring-boot:run
```
With Gradle, the command is as follows:
```bash
./gradlew bootRun
```
You can then access the Petclinic at <http://localhost:8080/>.
<img width="1042" alt="petclinic-screenshot" src="https://cloud.githubusercontent.com/assets/838318/19727082/2aee6d6c-9b8e-11e6-81fe-e889a5ddfded.png">
You can, of course, run Petclinic in your favorite IDE.
See below for more details.
## Building a Container
There is no `Dockerfile` in this project. You can build a container image (if you have a docker daemon) using the Spring Boot build plugin:
```bash
./mvnw spring-boot:build-image
```
## In case you find a bug/suggested improvement for Spring Petclinic
Our issue tracker is available [here](https://github.com/spring-projects/spring-petclinic/issues).
## Database configuration
In its default configuration, Petclinic uses an in-memory database (H2) which
gets populated at startup with data. The h2 console is exposed at `http://localhost:8080/h2-console`,
and it is possible to inspect the content of the database using the `jdbc:h2:mem:<uuid>` URL. The UUID is printed at startup to the console.
gets populated at startup with data.
A similar setup is provided for MySQL and PostgreSQL if a persistent database configuration is needed. Note that whenever the database type changes, the app needs to run with a different profile: `spring.profiles.active=mysql` for MySQL or `spring.profiles.active=postgres` for PostgreSQL. See the [Spring Boot documentation](https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html#howto.properties-and-configuration.set-active-spring-profiles) for more detail on how to set the active profile.
You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker:
I have used MySQL 8.0.34 for development and testing.
For which I have added the following configuration to `src/main/resources/application-mysql.properties`:
```bash
docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:9.5
```properties
spring.jpa.hibernate.ddl-auto=update
```
or
Before running the application with MySQL, make sure to create a database named `petclinic` if required.
### Build & Run
You can start the application using maven on the command-line as follows:
```bash
docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:18.1
./mvnw spring-boot:run
```
Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt)
and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt).
Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a service named after the Spring profile:
If you are using MySQL, you can start the application with the MySQL profile:
```bash
docker compose up mysql
./mvnw spring-boot:run -Dspring-boot.run.profiles=mysql
```
You can then access the Petclinic at <http://localhost:8080/>.
or
---
```bash
docker compose up postgres
```
## Test Applications
## Feature Flags Implemented
At development time we recommend you use the test applications set up as `main()` methods in `PetClinicIntegrationTests` (using the default H2 database and also adding Spring Boot Devtools), `MySqlTestApplication` and `PostgresIntegrationTests`. These are set up so that you can run the apps in your IDE to get fast feedback and also run the same classes as integration tests against the respective database. The MySql integration tests use Testcontainers to start the database in a Docker container, and the Postgres tests use Docker Compose to do the same thing.
## Compiling the CSS
| Feature | Flag Name | Location |
|--------------|--------------|------------------------------------------|
| Add Pet | ADD_PET | PetController POST processCreationForm |
| Add Visit | ADD_VISIT | VisitController POST processNewVisitForm |
| Owner Search | OWNER_SEARCH | OwnerController GET processFindForm |
There is a `petclinic.css` in `src/main/resources/static/resources/css`. It was generated from the `petclinic.scss` source, combined with the [Bootstrap](https://getbootstrap.com/) library. If you make changes to the `scss`, or upgrade Bootstrap, you will need to re-compile the CSS resources using the Maven profile "css", i.e. `./mvnw package -P css`. There is no build profile for Gradle to compile the CSS.
## Working with Petclinic in your IDE
Each feature is protected using:
@FeatureSwitch("FLAG_NAME")
### Prerequisites
---
The following items should be installed in your system:
- Java 17 or newer (full JDK, not a JRE)
- [Git command line tool](https://help.github.com/articles/set-up-git)
- Your preferred IDE
- Eclipse with the m2e plugin. Note: when m2e is available, there is a m2 icon in `Help -> About` dialog. If m2e is
not there, follow the installation process [here](https://www.eclipse.org/m2e/)
- [Spring Tools Suite](https://spring.io/tools) (STS)
- [IntelliJ IDEA](https://www.jetbrains.com/idea/)
- [VS Code](https://code.visualstudio.com)
## Feature Flag Capabilities
### Steps
1. On the command line run:
### Global Enable/Disable
Turns feature fully on/off.
```bash
git clone https://github.com/spring-projects/spring-petclinic.git
```
1. Inside Eclipse or STS:
### Whitelist
Specific users always enabled.
Open the project via `File -> Import -> Maven -> Existing Maven project`, then select the root directory of the cloned repo.
Then either build on the command line `./mvnw generate-resources` or use the Eclipse launcher (right-click on project and `Run As -> Maven install`) to generate the CSS. Run the application's main method by right-clicking on it and choosing `Run As -> Java Application`.
### Blacklist
Specific users always blocked.
1. Inside IntelliJ IDEA:
In the main menu, choose `File -> Open` and select the Petclinic [pom.xml](pom.xml). Click on the `Open` button.
### Percentage Rollout
Deterministic hash-based rollout.
- CSS files are generated from the Maven build. You can build them on the command line `./mvnw generate-resources` or right-click on the `spring-petclinic` project then `Maven -> Generates sources and Update Folders`.
- A run configuration named `PetClinicApplication` should have been created for you if you're using a recent Ultimate version. Otherwise, run the application by right-clicking on the `PetClinicApplication` main class and choosing `Run 'PetClinicApplication'`.
---
1. Navigate to the Petclinic
Visit [http://localhost:8080](http://localhost:8080) in your browser.
## API — Feature Flag Management
## Looking for something in particular?
|Spring Boot Configuration | Class or Java property files |
|--------------------------|---|
|The Main Class | [PetClinicApplication](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java) |
|Properties Files | [application.properties](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources) |
|Caching | [CacheConfiguration](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java) |
Base Path:
`/api/flags`
## Interesting Spring Petclinic branches and forks
### Create Flag
The Spring Petclinic "main" branch in the [spring-projects](https://github.com/spring-projects/spring-petclinic)
GitHub org is the "canonical" implementation based on Spring Boot and Thymeleaf. There are
[quite a few forks](https://spring-petclinic.github.io/docs/forks.html) in the GitHub org
[spring-petclinic](https://github.com/spring-petclinic). If you are interested in using a different technology stack to implement the Pet Clinic, please join the community there.
## Interaction with other open-source projects
POST `/api/flags`
One of the best parts about working on the Spring Petclinic application is that we have the opportunity to work in direct contact with many Open Source projects. We found bugs/suggested improvements on various topics such as Spring, Spring Data, Bean Validation and even Eclipse! In many cases, they've been fixed/implemented in just a few days.
Here is a list of them:
`{ "flagName": "ADD_PET", "flagEnabled": true, "rolloutPercentage": 100 }`
| Name | Issue |
|------|-------|
| Spring JDBC: simplify usage of NamedParameterJdbcTemplate | [SPR-10256](https://github.com/spring-projects/spring-framework/issues/14889) and [SPR-10257](https://github.com/spring-projects/spring-framework/issues/14890) |
| Bean Validation / Hibernate Validator: simplify Maven dependencies and backward compatibility |[HV-790](https://hibernate.atlassian.net/browse/HV-790) and [HV-792](https://hibernate.atlassian.net/browse/HV-792) |
| Spring Data: provide more flexibility when working with JPQL queries | [DATAJPA-292](https://github.com/spring-projects/spring-data-jpa/issues/704) |
Returns **201 CREATED**
## Contributing
The [issue tracker](https://github.com/spring-projects/spring-petclinic/issues) is the preferred channel for bug reports, feature requests and submitting pull requests.
---
For pull requests, editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org>. All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
## License
### List Flags
The Spring PetClinic sample application is released under version 2.0 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0).
GET `/api/flags`
---
### List Flag based on flag name
GET `/api/flags/{name}`
GET `/api/flags`
---
### Delete Flag
DELETE `/api/flags/{name}`
Returns **204 NO CONTENT**
---
## Design Decisions
- Built custom annotation + AOP instead of libraries (per requirement)
- DB-backed persistence
- Deterministic rollout using hash bucket
- Default behavior = disabled if flag missing
- No authentication on flag APIs (as per requirement)
---
## Edge Cases Handled
- Missing flag is treated as feature disabled
- 0% rollout is treated as always blocked
- 100% rollout is always enabled
- Blacklist overrides whitelist
---
## Package Structure (New Code)
The new code is placed in a separate package to maintain modularity and separation of concerns:
- `org.springframework.examples.petclinic.featureflags` — Core feature flag system (annotation, aspect, service, repository)
## Credits
Base project forked from:
Spring PetClinic — https://github.com/spring-projects/spring-petclinic
Feature Flag system implementation added as part of assignment work.

View file

@ -0,0 +1,14 @@
package org.springframework.samples.petclinic.featureflag.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureSwitch {
String value();
}

View file

@ -0,0 +1,30 @@
package org.springframework.samples.petclinic.featureflag.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureSwitch;
import org.springframework.samples.petclinic.featureflag.constants.FeatureFlagConstants;
import org.springframework.samples.petclinic.featureflag.service.FeatureEvaluationService;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class FeatureSwitchAspect {
@Autowired
public FeatureEvaluationService featureEvaluationService;
@Around("@annotation(featureSwitch)")
public Object checkFeatureFlag(ProceedingJoinPoint joinPoint, FeatureSwitch featureSwitch) throws Throwable {
String user = FeatureFlagConstants.ANONYMOUS_USER;
if (featureEvaluationService.isFlagEnabledForUser(featureSwitch.value(), user)) {
return joinPoint.proceed();
}
else {
throw new RuntimeException("Feature " + featureSwitch.value() + " is disabled");
}
}
}

View file

@ -0,0 +1,16 @@
package org.springframework.samples.petclinic.featureflag.constants;
public class FeatureFlagConstants {
private FeatureFlagConstants() {
}
public static final String ADD_PET = "ADD_PET";
public static final String ADD_VISIT = "ADD_VISIT";
public static final String OWNER_SEARCH = "OWNER_SEARCH";
public static final String ANONYMOUS_USER = "anonymous";
}

View file

@ -0,0 +1,47 @@
package org.springframework.samples.petclinic.featureflag.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.featureflag.entity.FeatureFlag;
import org.springframework.samples.petclinic.featureflag.service.FeatureFlagService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/flags")
public class FeatureFlagController {
@Autowired
private FeatureFlagService service;
@PostMapping
public ResponseEntity<FeatureFlag> createFeatureFlag(@RequestBody FeatureFlag flag) {
FeatureFlag createdFlag = service.createFeatureFlag(flag);
return ResponseEntity.status(HttpStatus.CREATED).body(createdFlag);
}
@GetMapping
public List<FeatureFlag> fetchAllFeatureFlags() {
return service.fetchAllFeatureFlags();
}
@GetMapping("/{name}")
public FeatureFlag getFeatureFlagByName(@PathVariable String name) {
return service.getFeatureFlagByName(name);
}
@PutMapping("/{id}")
public ResponseEntity<FeatureFlag> updateFeatureFlag(@PathVariable Long id, @RequestBody FeatureFlag flag) {
return service.updateFeatureFlag(id, flag).map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteFeatureFlag(@PathVariable Long id) {
service.deleteFeatureFlag(id);
return ResponseEntity.noContent().build();
}
}

View file

@ -0,0 +1,98 @@
package org.springframework.samples.petclinic.featureflag.entity;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "feature_flags")
public class FeatureFlag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long flagId;
private String flagName;
private boolean flagEnabled;
private Integer rolloutPercentage;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "flag_whitelist")
private Set<String> whitelistUsers = new HashSet<>();
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "flag_blacklist")
private Set<String> blacklistUsers = new HashSet<>();
private String flagDescription;
public FeatureFlag() {
}
public Long getFlagId() {
return flagId;
}
public void setFlagId(Long flagId) {
this.flagId = flagId;
}
public String getFlagName() {
return flagName;
}
public void setFlagName(String flagName) {
this.flagName = flagName;
}
public boolean isFlagEnabled() {
return flagEnabled;
}
public void setFlagEnabled(boolean flagEnabled) {
this.flagEnabled = flagEnabled;
}
public Integer getRolloutPercentage() {
return rolloutPercentage;
}
public void setRolloutPercentage(Integer rolloutPercentage) {
this.rolloutPercentage = rolloutPercentage;
}
public Set<String> getWhitelistUsers() {
return whitelistUsers;
}
public void setWhitelistUsers(Set<String> whitelistUsers) {
this.whitelistUsers = whitelistUsers;
}
public Set<String> getBlacklistUsers() {
return blacklistUsers;
}
public void setBlacklistUsers(Set<String> blacklistUsers) {
this.blacklistUsers = blacklistUsers;
}
public String getFlagDescription() {
return flagDescription;
}
public void setFlagDescription(String flagDescription) {
this.flagDescription = flagDescription;
}
@Override
public String toString() {
return "FeatureFlag{" + "flagId=" + flagId + ", flagName='" + flagName + '\'' + ", isFlagEnabled=" + flagEnabled
+ ", rolloutPercentage=" + rolloutPercentage + ", whitelistUsers=" + whitelistUsers
+ ", blacklistUsers=" + blacklistUsers + ", flagDescription='" + flagDescription + '\'' + '}';
}
}

View file

@ -0,0 +1,22 @@
package org.springframework.samples.petclinic.featureflag.helper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.featureflag.service.FeatureEvaluationService;
import org.springframework.stereotype.Component;
@Component
public class FeatureFlagHelper {
@Autowired
private FeatureEvaluationService service;
public boolean isEnabledForUser(String flagName) {
String user = getCurrentUser();
return service.isFlagEnabledForUser(flagName, user);
}
private String getCurrentUser() {
return "anonymous";
}
}

View file

@ -0,0 +1,14 @@
package org.springframework.samples.petclinic.featureflag.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.samples.petclinic.featureflag.entity.FeatureFlag;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface FeatureFlagRepository extends JpaRepository<FeatureFlag, Long> {
Optional<FeatureFlag> findByFlagName(String flagName);
}

View file

@ -0,0 +1,53 @@
package org.springframework.samples.petclinic.featureflag.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.featureflag.entity.FeatureFlag;
import org.springframework.samples.petclinic.featureflag.repository.FeatureFlagRepository;
import org.springframework.stereotype.Service;
/**
* Service responsible for evaluating feature flags.
* Supports global enable/disable, whitelist, blacklist,
* and percentage rollout strategies.
*/
@Service
public class FeatureEvaluationService {
@Autowired
private FeatureFlagRepository featureFlagRepository;
/**
* Determines whether a feature is enabled for a given user.
*
* Evaluation order:
* 1. Global disable check
* 2. Blacklist override
* 3. Whitelist override
* 4. Percentage rollout
*/
public boolean isFlagEnabledForUser(String flagName, String userId) {
FeatureFlag flag = featureFlagRepository.findByFlagName(flagName).orElse(null);
if (flag == null)
return false;
if (!flag.isFlagEnabled())
return false;
if (flag.getBlacklistUsers().contains(userId))
return false;
if (flag.getWhitelistUsers().contains(userId))
return true;
Integer rolloutPercentage = flag.getRolloutPercentage();
if (rolloutPercentage != null) {
if (rolloutPercentage < 0 || rolloutPercentage > 100)
return false;
int bucket = Math.abs(userId.hashCode() % 100);
return bucket < rolloutPercentage;
}
return true;
}
}

View file

@ -0,0 +1,65 @@
package org.springframework.samples.petclinic.featureflag.service;
import org.springframework.samples.petclinic.featureflag.entity.FeatureFlag;
import org.springframework.samples.petclinic.featureflag.repository.FeatureFlagRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class FeatureFlagService {
private final FeatureFlagRepository repo;
public FeatureFlagService(FeatureFlagRepository repo) {
this.repo = repo;
}
public FeatureFlag createFeatureFlag(FeatureFlag flag) {
validatePercentageRollout(flag);
return repo.save(flag);
}
public List<FeatureFlag> fetchAllFeatureFlags() {
return repo.findAll();
}
public FeatureFlag getFeatureFlagByName(String name) {
return repo.findByFlagName(name).orElseThrow();
}
public Optional<FeatureFlag> updateFeatureFlag(Long id, FeatureFlag flag) {
Optional<FeatureFlag> optionalFlag = repo.findById(id);
if (optionalFlag.isEmpty()) {
return Optional.empty();
}
FeatureFlag f = optionalFlag.get();
if (flag.getFlagDescription() != null) {
f.setFlagDescription(flag.getFlagDescription());
}
f.setFlagEnabled(flag.isFlagEnabled());
f.setRolloutPercentage(flag.getRolloutPercentage());
f.setWhitelistUsers(flag.getWhitelistUsers());
f.setBlacklistUsers(flag.getBlacklistUsers());
FeatureFlag saved = repo.save(f);
return Optional.of(saved);
}
public void deleteFeatureFlag(Long id) {
repo.deleteById(id);
}
private void validatePercentageRollout(FeatureFlag flag) {
Integer pct = flag.getRolloutPercentage();
if (pct != null && (pct < 0 || pct > 100)) {
throw new IllegalArgumentException("rolloutPercentage must be 0-100");
}
}
}

View file

@ -22,6 +22,8 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureSwitch;
import org.springframework.samples.petclinic.featureflag.constants.FeatureFlagConstants;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -91,6 +93,7 @@ class OwnerController {
return "owners/findOwners";
}
@FeatureSwitch(FeatureFlagConstants.OWNER_SEARCH)
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
Model model) {

View file

@ -20,6 +20,8 @@ import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureSwitch;
import org.springframework.samples.petclinic.featureflag.constants.FeatureFlagConstants;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
@ -102,6 +104,7 @@ class PetController {
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@FeatureSwitch(FeatureFlagConstants.ADD_PET)
@PostMapping("/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
RedirectAttributes redirectAttributes) {

View file

@ -18,6 +18,8 @@ package org.springframework.samples.petclinic.owner;
import java.util.Map;
import java.util.Optional;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureSwitch;
import org.springframework.samples.petclinic.featureflag.constants.FeatureFlagConstants;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
@ -88,6 +90,7 @@ class VisitController {
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
// called
@FeatureSwitch(FeatureFlagConstants.ADD_VISIT)
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit,
BindingResult result, RedirectAttributes redirectAttributes) {

View file

@ -5,3 +5,4 @@ spring.datasource.username=${MYSQL_USER:petclinic}
spring.datasource.password=${MYSQL_PASS:petclinic}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
spring.jpa.hibernate.ddl-auto=update