Enable and disable endpoints at runtime with Spring boot - spring

Let's say I have the following controller:
#RestController
public class MyController {
#GetMapping("v1/remain")
public MyObject getRemain() {
// ...
}
}
How can I enable or disable this endpoint at runtime dynamically with Spring boot? Also, is it possible to change this without having to restart the application?

You can either use #ConditionalOnExpression or #ConditionalOnProperty
#RestController
#ConditionalOnExpression("${my.property:false}")
#RequestMapping(value = "my-end-point", produces = MediaType.APPLICATION_JSON_VALUE)
public class MyController {
#RequestMapping(value = "endpoint1", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> endpoint1(
return new ResponseEntity<>("Hello world", HttpStatus.OK);
}
}
Now if you want the above controller to work, you need to add following in application.properties file.
my.controller.enabled=true
Without the above statement, it will behave like the above controller don't exist.
Similiarly,
#ConditionalOnProperty("my.property")
behaves exactly same as above; if the property is present and "true", the component works, otherwise it doesn't.

To dynamically reload beans when a property changes, you could use Spring boot actuator + Spring cloud so that you have access to the /actuator/refresh endpoint.
This can be done by adding the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
The latter does require that you add the BOM for Spring cloud, which is:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Now you can enable the /actuator/refresh endpoint by setting the following property:
management.endpoints.web.exposure.include=refresh
This will allow you to send a POST call to /actuator/refresh, which will return an array of all changed properties.
By using the /actuator/refresh endpoint, it also allows you to use the #RefreshScope annotation to recreate beans. However, there are a few limitations:
#RefreshScope recreates the bean without re-evaluating conditionals that might have changed due to the refresh. That means that this solution doesn't work with #RefreshScope, as seen in the comment section of this question.
#RefreshScope doesn't work nicely with filters either, as seen in this issue.
That means you have two options:
Add the #RefreshScope to the controller and do the conditional logic by yourself, for example:
#RefreshScope
#RestController
#RequestMapping("/api/foo")
public class FooController {
#Value("${foo.controller.enabled}")
private boolean enabled;
#GetMapping
public ResponseEntity<String> getFoo() {
return enabled ? ResponseEntity.of("bar") : ResponseEntity.notFound().build();
}
}
This means you would have to add this condition to all endpoints within your controller. I haven't verified if you could use this with aspects.
Another solution is to not use #RefreshScope to begin with, and to lazily fetch the property you want to validate. This allows you to use it with a filter, for example:
public class FooFilter extends OncePerRequestFilter {
private Environment environment;
public FooFilter(Environment environment) {
this.environment = environment;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if ("true".equalsIgnoreCase(environment.getProperty("foo.controller.enabled"))) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}
You'll have to register the filter as well, for example by using:
#Bean
public FilterRegistrationBean<FooFilter> fooFilter(Environment environment) {
FilterRegistrationBean<FooFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new FooFilter(environment));
bean.addUrlPatterns("/api/foo");
return bean;
}
Please note, this approach only fetches the property dynamically from the Environment. Refreshing the Environment itself still requires you to use the /actuator/refresh endpoint.

Related

How to correctly handle "FeignException$ServiceUnavailableException" with fallback and without deprecated #EnableCircuitbreaker annotation

When using OpenFeign I implement fallbacks returning empty results so lists should simply appear as being empty. E.g. such as
#FeignClient(name = "objects", fallback = ObjectsClientFallback.class)
public interface ObjectsClient {
#RequestMapping("/objects/count")
Long count();
}
and
#Component
public class ObjectsClientFallback implements ObjectsClient {
#Override
public Long count() {
return 0L;
}
}
However, if the service is not started the application produces the ServiceUnavailableException when calling objectsClient.count() instead of using the fallback.
What is the correct way to use the fallback as #EnableCircuitBreaker has been deprecated recently? I do not want to add try-catch blocks if possible, especially in the context of lambdas or wrap this in service methods.
The application uses the #EnableDiscoveryClient annotation, like so
#SpringBootApplication
#EnableJpaRepositories
#EnableFeignClients
#EnableDiscoveryClient
#ServletComponentScan
public class Application {
//..
}
I've seen this question and checked the mentioned documentation, but it didn't help. Library versions are Spring-Boot 2.6.2 and Spring-Cloud 2021.0.0
Ended up using resilience4j for spring cloud.
Next to feign.circuitbreaker.enabled=true in application.properties which sets up a circuitbreaker with a default config.
One can add a custom configuration like this:
#Configuration
public class FeignConfiguration {
#Bean
public Customizer<Resilience4JCircuitBreakerFactory> circuitBreakerFactoryCustomizer() {
CircuitBreakerConfig circuitBreakerConfig =
CircuitBreakerConfig.custom()
.ignoreException(FeignException.ServiceUnavailable.class::isInstance)
.build();
return circuitBreakerFactory -> circuitBreakerFactory
.configure(builder -> builder.circuitBreakerConfig(circuitBreakerConfig),
ObjectsClient.class.getSimpleName());
}
}
Also, the resilience4j dependency needs to be in place, here a maven pom.xml is used.
<project>
<!-- .. -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>
That way you can replace the deprecated hystrix implementation and remove the #EnableCircuitBreaker annotation.
In case you want a more fine-grained setup, here are is a discussion how to implement fallback with resilience4j via default methods.

How to use load time weaving without -javaagent?

I am trying to enable loadtimeweaving without javaagent jar files of aspectweaver and spring-instrument. This what I have implemented to achieve the same But it's not working.
#ComponentScan("com.myapplication")
#EnableAspectJAutoProxy
#EnableSpringConfigured
#EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.AUTODETECT)
public class AopConfig implements LoadTimeWeavingConfigurer {
#Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
/**
* Makes the aspect a Spring bean, eligible for receiving autowired components.
*/
#Bean
public InstrumentationLoadTimeWeaver loadTimeWeaver() throws Throwable {
InstrumentationLoadTimeWeaver loadTimeWeaver = new InstrumentationLoadTimeWeaver();
return loadTimeWeaver;
}
}
A workaround I found was to hot-attach InstrumentationSavingAgent from spring-instrument instead of starting the agent via -javaagent command line parameter. But for that you need an Instrumentation instance. I just used the tiny helper library byte-buddy-helper (works independently of ByteBuddy, don't worry) which can do just that. Make sure that in Java 9+ JVMs the Attach API is activated if for some reason this is not working.
So get rid of implements LoadTimeWeavingConfigurer and the two factory methods in your configuration class and just do it like this:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.10.14</version>
</dependency>
#SpringBootApplication
public class Application {
public static void main(String[] args) {
Instrumentation instrumentation = ByteBuddyAgent.install();
InstrumentationSavingAgent.premain("", instrumentation);
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
// ...
}
}
Feel free to ask follow-up questions if there is anything you do not understand.
Update: One more thing I noticed is that this only works for me with aspectjWeaving = ENABLED, not with AUTODETECT. And for one sample Spring bean I noticed that #Component did not work, probably because of some bootstrapping issue between Spring vs AspectJ. Hence, I replaced it by an explicit #Bean configuration, then it worked. Something like this:
#Configuration
#ComponentScan("com.spring.aspect.dynamicflow")
#EnableLoadTimeWeaving(aspectjWeaving = ENABLED)
public class ApplicationConfig {
#Bean
public JobProcess jobProcess() {
return new JobProcessImpl();
}
}

Spring boot cache results

I have several read-only resources in my Controller. I want to in-memory cache them, I'm not clear how to do it in Spring Boot.
What I've done:
annotated main Application with #EnableCaching
annotated a resource with #Cacheable
#Cacheable
#RequestMapping(value = "/api/graph", method=RequestMethod.GET, produces = { "application/json"})
public #ResponseBody Iterable<Map<String, String>> graph() {
return Repository.graph();
}
What am I missing?
Since it's a read-only resource I guess I don't neet #CachePut am I right?
Obiouvsly I added spring-boot-starter-cache ad dependency in maven
Here are the steps to enable the Cache. For example i am using Google GuavaCacheManager for this purpose.
Add dependency and Enable the caching by using annotation on Application class.
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
#SpringBootApplication
#EnableCaching
public class Application extends SpringBootServletInitializer {
}
Expose the GuavaCacheManager as a bean in your Application class.
#Bean
public CacheManager cacheManager() {
GuavaCacheManager cacheManager = new GuavaCacheManager();
// Cache expires every day
cacheManager.setCacheBuilder(CacheBuilder.newBuilder().expireAfterAccess(1,
TimeUnit.DAYS).expireAfterWrite(1, TimeUnit.DAYS));
cacheManager.setCacheNames(Arrays.asList("findUser"));
return cacheManager;
}
Mark the method as Cacheable so all the calls to this method first try to find the entry in Cache if not found then actually calls the method
#Override
#Cacheable("findUser")
public User findUser(String username) {
// biz logic to find the user and return the object
return user;
}

Why does spring rest controller return unauthorized when no security has been set

I have start a spring boot application. I added a rest controller like this
#RestController
public class EmailController {
#RequestMapping(value = "/2", method = RequestMethod.GET)
public int getNumber() {
return 2;
}
}
When I call the url http://localhost:8080/2 I get a 401 exception.
{"timestamp":1498409208660,"status":401,"error":
"Unauthorized","message":"Full authentication is
required to access this resource", "path":"/2"}
That I have absolutely no security set whats so ever. This is a clean spring boot project! Why am I receiving an unauthorized exception.
You configured no authentication (Form Login, HTTP Basic, ...) so the default AuthenticationEntryPointis used, see Spring Security API here:
Sets the AuthenticationEntryPoint to be used.
If no authenticationEntryPoint(AuthenticationEntryPoint) is specified, then defaultAuthenticationEntryPointFor(AuthenticationEntryPoint, RequestMatcher) will be used. The first AuthenticationEntryPoint will be used as the default is no matches were found.
If that is not provided defaults to Http403ForbiddenEntryPoint.
You can set the AuthenticationEntryPoint as #ksokol wrote or configure a authentication, which defines a AuthenticationEntryPoint.
Add #EnableWebSecurity annotation in your main class it will ignore default security to access that application.
sample code is below:
#EnableWebSecurity
#SpringBootApplication
public class SampleApplication {
SampleApplication (){}
public static void main(String[] args) {
SpringApplication.run(SampleApplication .class, args);
}
For people looking for a solution. I added this to my application.yml
security:
ignored: /**
Check that in the pom.xml file you do not include the dependency spring-boot-starter-security.
Because you added Spring Security Dependency to your project but didn't config it.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring boot & Swagger 2 UI & custom requestmappinghandlermapping - mapping issue

I have own RequestMappingHandlerMapping and I am using springfox-swagger-ui. After adding my custom mapping, I am not able to achieve swagger ui at http://localhost:8080/swagger-ui.html.
Any ideas?
This is my configuration.
#Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
#Override
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping("v");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/webjars/**")
.addResourceLocations("(META-INF/resources/webjars");
}
}
Here's my pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
When you override WebMvcConfigurationSupport, you are also overriding spring Boot's mvc auto configuration (WebMvcAutoConfiguration). Therefore, resources that need spring boot's configuration will not work. This is not a problem specific to swagger.
You can find more info about this here:
https://github.com/spring-projects/spring-boot/issues/5004
As the github issue suggests, there will be changes on this in the future to make it easier. Currently there are some workarounds, as suggested there.
A quick and dirty way of doing this is by copying and pasting the WebMvcAutoConfiguration class into your own class, returning your own HandlerMapping from the requestMappingHandlerMapping() method of EnableWebMvcConfiguration and registering the copy of the WebMvcAutoConfiguration as an auto configuration class. You can see instructions here:
http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html
Make sure you place your copy of the WebMvcAutoConfiguration at some package which is not component scanned and picked up automatically. It should just be registered as explained in the above link.
Also make sure you set the order of you custom HandlerMapping to 0 before returning it from the requestMappingHandlerMapping() method, like so:
#Bean
#Primary
#Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// Must be #Primary for MvcUriComponentsBuilder to work
ApiVersionRequestMappingHandlerMapping handlerMapping = new ApiVersionRequestMappingHandlerMapping("v");
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager());
PathMatchConfigurer configurer = getPathMatchConfigurer();
if (configurer.isUseSuffixPatternMatch() != null) {
handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
}
if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
handlerMapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
}
if (configurer.isUseTrailingSlashMatch() != null) {
handlerMapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
}
if (configurer.getPathMatcher() != null) {
handlerMapping.setPathMatcher(configurer.getPathMatcher());
}
if (configurer.getUrlPathHelper() != null) {
handlerMapping.setUrlPathHelper(configurer.getUrlPathHelper());
}
return handlerMapping;
}
It works for me.
Overriding addResourceHandlers instead of registering auto-configuration.
Github Source
#Configuration
#EnableWebMvc
#EnableSwagger2
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Overriding requestMappingHandlerMapping() of WebMvcConfigurationSupport will turn off spring boot's auto configuration. For adding custom MVC Components you may use WebMvcRegistrations. Like, for providing custom RequestMappingHandlerMapping, We may override getRequestMappingHandlerMapping(), with custom RequestMappingHandlerMapping, ofWebMvcRegistrationsAdapter and provide it through webMvcRegistrationsHandlerMapping(). As,
#Configuration
class CustomRequestMappingHandlerMapping {
#Bean
public WebMvcRegistrationsAdapter webMvcRegistrationsHandlerMapping() {
return new WebMvcRegistrationsAdapter() {
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping("v");
}
};
}
}
In Spring Boot 2.0.0, there is a simpler way to achieve this.
Create an instance of WebMvcRegistrations interface as a bean and override appropriate method to return the customized version of that object. Spring boot will read and use that instance.
In this case only the getRequestMappingHandlerMapping() needs to be overridden and a custom implementation returned
Above information is from a follow through based on the links provided by #Nazaret K.
More information at https://github.com/spring-projects/spring-boot/issues/5004
It can be solved by using WebMvcConfigurationSupport with adding resource handlers for swagger:
#Configuration
public class MvcConfiguration extends WebMvcConfigurationSupport {
#Value("${spring.application.name}")
private String applicationName;
//...irrelevant code here
#Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
I finally find it!
The right configuration is this:
#Configuration
public class VersioningMappingHandlerConfig {
#Bean
public ApiVersionRequestMappingHandlerMapping customMappingHandlerMapping() {
ApiVersionRequestMappingHandlerMapping handler = new ApiVersionRequestMappingHandlerMapping("v", 1, 1);
handler.setOrder(-1);
return handler;
}
}
Note: there is no extends WebMvcConfigurationSupport and bean name is customMappingHandlerMapping

Resources