Spring AOP NullPointerException after running successfully for an extended period of time - spring-boot

This is a problem which has stumped myself and two of my colleagues for a few days now.
We are receiving a NullPointerException after our spring-boot microservice has been running without a hitch anywhere from a few minutes to a few hours and has received a few hundred to few thousand requests. This issue started after a few beans were changed to being request-scoped due to a requirements change.
Classes (all objects are autowired/constructed at microservice boot):
// New class introduced to accommodate requirements change.
#Repository("databaseUserAccountRepo")
public class DatabaseAccountUserRepoImpl implements UserLdapRepo {
private final DatabaseAccountUserRepository databaseAccountUserRepository;
#Autowired
public DatabaseAccountUserRepoImpl(
#Qualifier("databaseAccountUserRepositoryPerRequest") final DatabaseAccountUserRepository databaseAccountUserRepository
) {
this.databaseAccountUserRepository = databaseAccountUserRepository;
}
// ...snip...
}
// ==============================================================================
// New class introduced to accommodate requirements change.
#Repository("databaseAccountUserRepository")
public interface DatabaseAccountUserRepository
extends org.springframework.data.repository.CrudRepository {
// ...snip...
}
// ==============================================================================
#Repository("ldapUserAccountRepo")
public class UserLdapRepoImpl implements UserLdapRepo {
// ...snip...
}
// ==============================================================================
#Component
public class LdapUtils {
private final UserLdapRepo userLdapRepo;
#Autowired
public LdapUtils(
#Qualifier("userLdapRepoPerRequest") final UserLdapRepo userLdapRepo
) {
this.userLdapRepo = userLdapRepo;
}
// ...snip...
public Object myMethod(/* whatever */) {
// ...snip...
return userLdapRepo.someMethod(/* whatever */);
}
}
// ==============================================================================
// I have no idea why the original developer decided to do it this way.
// It's worked fine up until now so I see no reason to change it unless
// I really need to.
public class AuthenticationContext {
private static final ThreadLocal<String> organizationNameThreadLocal = new ThreadLocal<>();
// ...snip...
public static void setOrganizationName(String organizationName) {
organizationNameThreadLocal.set(organizationName);
}
public static String getOrganizationName() {
return organizationNameThreadLocal.get();
}
public static void clear() {
organizationNameThreadLocal.remove();
}
// ...snip...
}
// ==============================================================================
public class AuthenticationContextInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
AuthenticationContext.setOrganizationName(request.getHeader("customer-id"));
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
AuthenticationContext.clear();
}
}
Code to request-scope:
#Configuration
// We have some aspects in our codebase, so this might be relevant.
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class ServiceConfiguration {
// ...snip...
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserLdapRepo userLdapRepoPerRequest(
final Map<String, String> customerIdToUserLdapRepoBeanName
) {
final String customerId = AuthenticationContext.getOrganizationName();
final String beanName = customerIdToUserLdapRepoBeanName.containsKey(customerId)
? customerIdToUserLdapRepoBeanName.get(customerId)
: customerIdToUserLdapRepoBeanName.get(null); // default
return (UserLdapRepo) applicationContext.getBean(beanName);
}
#Bean
public Map<String, String> customerIdToUserLdapRepoBeanName(
#Value("${customers.user-accounts.datastore.use-database}") final String[] customersUsingDatabaseForAccounts
) {
final Map<String, String> customerIdToUserLdapRepoBeanName = new HashMap<>();
customerIdToUserLdapRepoBeanName.put(null, "ldapUserAccountRepo"); // default option
if (customersUsingDatabaseForAccounts != null && customersUsingDatabaseForAccounts.length > 0) {
Arrays.stream(customersUsingDatabaseForAccounts)
.forEach(customerId ->
customerIdToUserLdapRepoBeanName.put(customerId, "databaseUserAccountRepo")
);
}
return customerIdToUserLdapRepoBeanName;
}
// Given a customer ID (taken from request header), returns the
// DatabaseAccountUserRepository instance for that particular customer.
// The DatabaseAccountUserRepositoryProvider is NOT request-scoped.
// The DatabaseAccountUserRepositoryProvider is basically just a utility
// wrapper around a map of String -> DatabaseAccountUserRepository.
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public DatabaseAccountUserRepository databaseAccountUserRepositoryPerRequest(
final DatabaseAccountUserRepositoryProvider databaseAccountUserRepositoryProvider
) {
final String customerId = AuthenticationContext.getOrganizationName();
return databaseAccountUserRepositoryProvider.getRepositoryFor(customerId);
}
// ...snip...
}
The stack trace:
java.lang.NullPointerException: null
at org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry.getInterceptors(DefaultAdvisorAdapterRegistry.java:81)
at org.springframework.aop.framework.DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(DefaultAdvisorChainFactory.java:89)
at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:489)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:659)
at com.mycompany.project.persistence.useraccount.ldap.UserLdapRepoImpl$$EnhancerBySpringCGLIB$$b6378f51.someMethod(<generated>)
at sun.reflect.GeneratedMethodAccessor304.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy209.findByFederatedInfo(Unknown Source)
at com.mycompany.project.util.LdapUtils.myMethod(LdapUtils.java:141)
The method in which the NPE is thrown is this guy:
//////////////////////////////////////////
// This is a method in Spring framework //
//////////////////////////////////////////
#Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
Advice advice = advisor.getAdvice(); // <<<<<<<<<< line 81
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
}
Most relevant dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- this results in spring-aop:4.3.14.RELEASE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
The request header customer-id is set by our proxy, so it must be available on the request (we added logging to verify that this statement is true; it is).
We do not know the exact traffic pattern which can cause the NPE to start being triggered. Once triggered, all subsequent requests also result in an NPE.
We have several other request-scoped beans in this project; they are also selected using the customer-id. Several of said objects have existed in this project for months prior to this change. They do not exhibit this problem.
We believe that the userLdapRepoPerRequest() and databaseAccountUserRepositoryPerRequest() methods are working correctly - receiving the correct customer-id, is returning the correct object, etc...at least when the methods are hit. This was determined by adding logging to the body of those methods - a log message immediately upon entering the method which records the parameter, one log message verifying value of the customer-id, and one log message immediately before returning which records the value which is to be returned. Note: Our logging setup has a correlation ID present on each message, so we can keep track of what messages corresponds the same request.
It's almost as if Spring is losing track of a few of its proxied beans.
Anyone have any ideas on what's happening or anything you would like us to try? Any leads are much appreciated.

Related

Why is exception in Spring Batch AsycItemProcessor caught by SkipListener's onSkipInWrite method?

I'm writing a Spring Boot application that starts up, gathers and converts millions of database entries into a new streamlined JSON format, and then sends them all to a GCP PubSub topic. I'm attempting to use Spring Batch for this, but I'm running into trouble implementing fault tolerance for my process. The database is rife with data quality issues, and sometimes my conversions to JSON will fail. When failures occur, I don't want the job to immediately quit, I want it to continue processing as many records as it can and, before completion, to report which exact records failed so that I, and or my team, can examine these problematic database entries.
To achieve this, I've attempted to use Spring Batch's SkipListener interface. But I'm also using an AsyncItemProcessor and an AsyncItemWriter in my process, and even though the exceptions are occurring during the processing, the SkipListener's onSkipInWrite() method is catching them - rather than the onSkipInProcess() method. And unfortunately, the onSkipInWrite() method doesn't have access to the original database entity, so I can't store its ID in my list of problematic DB entries.
Have I misconfigured something? Is there any other way to gain access to the objects from the reader that failed the processing step of an AsynItemProcessor?
Here's what I've tried...
I have a singleton Spring Component where I store how many DB entries I've successfully processed along with up to 20 problematic database entries.
#Component
#Getter //lombok
public class ProcessStatus {
private int processed;
private int failureCount;
private final List<UnexpectedFailure> unexpectedFailures = new ArrayList<>();
public void incrementProgress { processed++; }
public void logUnexpectedFailure(UnexpectedFailure failure) {
failureCount++;
unexpectedFailure.add(failure);
}
#Getter
#AllArgsConstructor
public static class UnexpectedFailure {
private Throwable error;
private DBProjection dbData;
}
}
I have a Spring batch Skip Listener that's supposed to catch failures and update my status component accordingly:
#AllArgsConstructor
public class ConversionSkipListener implements SkipListener<DBProjection, Future<JsonMessage>> {
private ProcessStatus processStatus;
#Override
public void onSkipInRead(Throwable error) {}
#Override
public void onSkipInProcess(DBProjection dbData, Throwable error) {
processStatus.logUnexpectedFailure(new ProcessStatus.UnexpectedFailure(error, dbData));
}
#Override
public void onSkipInWrite(Future<JsonMessage> messageFuture, Throwable error) {
//This is getting called instead!! Even though the exception happened during processing :(
//But I have no access to the original DBProjection data here, and messageFuture.get() gives me null.
}
}
And then I've configured my job like this:
#Configuration
public class ConversionBatchJobConfig {
#Autowired
private JobBuilderFactory jobBuilderFactory;
#Autowired
private StepBuilderFactory stepBuilderFactory;
#Autowired
private TaskExecutor processThreadPool;
#Bean
public SimpleCompletionPolicy processChunkSize(#Value("${commit.chunk.size:100}") Integer chunkSize) {
return new SimpleCompletionPolicy(chunkSize);
}
#Bean
#StepScope
public ItemStreamReader<DbProjection> dbReader(
MyDomainRepository myDomainRepository,
#Value("#{jobParameters[pageSize]}") Integer pageSize,
#Value("#{jobParameters[limit]}") Integer limit) {
RepositoryItemReader<DbProjection> myDomainRepositoryReader = new RepositoryItemReader<>();
myDomainRepositoryReader.setRepository(myDomainRepository);
myDomainRepositoryReader.setMethodName("findActiveDbDomains"); //A native query
myDomainRepositoryReader.setArguments(new ArrayList<Object>() {{
add("ACTIVE");
}});
myDomainRepositoryReader.setSort(new HashMap<String, Sort.Direction>() {{
put("update_date", Sort.Direction.ASC);
}});
myDomainRepositoryReader.setPageSize(pageSize);
myDomainRepositoryReader.setMaxItemCount(limit);
// myDomainRepositoryReader.setSaveState(false); <== haven't figured out what this does yet
return myDomainRepositoryReader;
}
#Bean
#StepScope
public ItemProcessor<DbProjection, JsonMessage> dataConverter(DataRetrievalSerivice dataRetrievalService) {
//Sometimes throws exceptions when DB data is exceptionally weird, bad, or missing
return new DbProjectionToJsonMessageConverter(dataRetrievalService);
}
#Bean
#StepScope
public AsyncItemProcessor<DbProjection, JsonMessage> asyncDataConverter(
ItemProcessor<DbProjection, JsonMessage> dataConverter) throws Exception {
AsyncItemProcessor<DbProjection, JsonMessage> asyncDataConverter = new AsyncItemProcessor<>();
asyncDataConverter.setDelegate(dataConverter);
asyncDataConverter.setTaskExecutor(processThreadPool);
asyncDataConverter.afterPropertiesSet();
return asyncDataConverter;
}
#Bean
#StepScope
public ItemWriter<JsonMessage> jsonPublisher(GcpPubsubPublisherService publisherService) {
return new JsonMessageWriter(publisherService);
}
#Bean
#StepScope
public AsyncItemWriter<JsonMessage> asyncJsonPublisher(ItemWriter<JsonMessage> jsonPublisher) throws Exception {
AsyncItemWriter<JsonMessage> asyncJsonPublisher = new AsyncItemWriter<>();
asyncJsonPublisher.setDelegate(jsonPublisher);
asyncJsonPublisher.afterPropertiesSet();
return asyncJsonPublisher;
}
#Bean
public Step conversionProcess(SimpleCompletionPolicy processChunkSize,
ItemStreamReader<DbProjection> dbReader,
AsyncItemProcessor<DbProjection, JsonMessage> asyncDataConverter,
AsyncItemWriter<JsonMessage> asyncJsonPublisher,
ProcessStatus processStatus,
#Value("${conversion.failure.limit:20}") int maximumFailures) {
return stepBuilderFactory.get("conversionProcess")
.<DbProjection, Future<JsonMessage>>chunk(processChunkSize)
.reader(dbReader)
.processor(asyncDataConverter)
.writer(asyncJsonPublisher)
.faultTolerant()
.skipPolicy(new MyCustomConversionSkipPolicy(maximumFailures))
// ^ for now this returns true for everything until 20 failures
.listener(new ConversionSkipListener(processStatus))
.build();
}
#Bean
public Job conversionJob(Step conversionProcess) {
return jobBuilderFactory.get("conversionJob")
.start(conversionProcess)
.build();
}
}
This is because the future wrapped by the AsyncItemProcessor is only unwrapped in the AsyncItemWriter, so any exception that might occur at that time is seen as a write exception instead of a processing exception. That's why onSkipInWrite is called instead of onSkipInProcess.
This is actually a known limitation of this pattern which is documented in the Javadoc of the AsyncItemProcessor, here is an excerpt:
Because the Future is typically unwrapped in the ItemWriter,
there are lifecycle and stats limitations (since the framework doesn't know
what the result of the processor is).
While not an exhaustive list, things like StepExecution.filterCount will not
reflect the number of filtered items and
itemProcessListener.onProcessError(Object, Exception) will not be called.
The Javadoc states that the list is not exhaustive, and the side-effect regarding the SkipListener that you are experiencing is one these limitations.

spring: customizing the authorizationEndpoint (OAuth2)

I am trying to customize the code of the spring oauth authorization server.
for now I have just copied the framework authorizationEndpoint code and placed it in another class. I just changed the address mapping to /custom/oauth/authorize. I have also added #Controller before the class declaration otherwise this code will not be used at all:
#Controller
//#Order(Ordered.HIGHEST_PRECEDENCE)
#SessionAttributes("authorizationRequest")
public class AuthorizationEndpointCustom extends AuthorizationEndpoint {
#Autowired
private AuthenticationManager authenticationManager;
private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();
private RedirectResolver redirectResolver = new DefaultRedirectResolver();
private UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();
private String userApprovalPage = "forward:/oauth/confirm_access";
private String errorPage = "forward:/oauth/error";
private Object implicitLock = new Object();
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
#RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, #RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
System.out.println("\n\ninside custom authorization endpoint");
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
OAuth2Request storedOAuth2Request) {
OAuth2AccessToken accessToken = null;
// These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
// one thread removes the token request before another has a chance to redeem it.
synchronized (this.implicitLock) {
accessToken = getTokenGranter().grant("implicit",
new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
}
return accessToken;
}
.
.
.
I have also instructed the framework to change the mappring from /oauth/authorize to /custom/oauth/authorize:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer()).pathMapping("/oauth/authorize", "/custom/authorize/");
}
but when I run the code I encounter the following error:
Description:
Field tokenGranter in com.example.demo.controller.AuthorizationEndpointCustom required a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' in your configuration.
the parent class of AuthorizationEndpoint (AbstractEndpoint) declares tokenGranter but it is not instantiated. there is no #autowired for this and other attributes of this class. who does genereate and inject these variable into this class?
how can I get hold of tokenGranter obj and inject it?
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.util.Assert;
/**
* #author Dave Syer
*
*/
public class AbstractEndpoint implements InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
private TokenGranter tokenGranter;
private ClientDetailsService clientDetailsService;
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2RequestFactory defaultOAuth2RequestFactory;
public void afterPropertiesSet() throws Exception {
Assert.state(tokenGranter != null, "TokenGranter must be provided");
Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(getClientDetailsService());
if (oAuth2RequestFactory == null) {
oAuth2RequestFactory = defaultOAuth2RequestFactory;
}
}
public void setProviderExceptionHandler(WebResponseExceptionTranslator providerExceptionHandler) {
this.providerExceptionHandler = providerExceptionHandler;
}
public void setTokenGranter(TokenGranter tokenGranter) {
this.tokenGranter = tokenGranter;
}
protected TokenGranter getTokenGranter() {
return tokenGranter;
}
protected WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
protected OAuth2RequestFactory getOAuth2RequestFactory() {
return oAuth2RequestFactory;
}
protected OAuth2RequestFactory getDefaultOAuth2RequestFactory() {
return defaultOAuth2RequestFactory;
}
public void setOAuth2RequestFactory(OAuth2RequestFactory oAuth2RequestFactory) {
this.oAuth2RequestFactory = oAuth2RequestFactory;
}
protected ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
I am answering my own question.
I took a good look at the framework code and I found out that AuthorizationServerEndpointsConfiguration class creates an object of type AuthorizationEndpoint and populates it's attributes and then return this object as a bean.
I managed to solve above mentioned problem with TokenGranter by creating a bean of my new AuthorizationEndpointCustom the same way AuthorizationServerEndpointsConfiguration does. this is the code to do so:
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
AuthorizationServerEndpointsConfiguration asec;
#Bean
#Order(value = Ordered.HIGHEST_PRECEDENCE)
#Primary
public AuthorizationEndpoint authorizationEndpoint () throws Exception{
AuthorizationEndpointCustom authorizationEndpoint = new AuthorizationEndpointCustom();
FrameworkEndpointHandlerMapping mapping = asec.getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(asec.getEndpointsConfigurer().getExceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(asec.getEndpointsConfigurer().getTokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(asec.getEndpointsConfigurer().getAuthorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(asec.getEndpointsConfigurer().getOAuth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(asec.getEndpointsConfigurer().getOAuth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(asec.getEndpointsConfigurer().getUserApprovalHandler());
return authorizationEndpoint;
}
private String extractPath(FrameworkEndpointHandlerMapping mapping, String page) {
String path = mapping.getPath(page);
if (path.contains(":")) {
return path;
}
return "forward:" + path;
}
but this did not result in what I hoped to. the new bean does not replace the bean from framework code. this situation with overriding beans led to another question:
how replace framework beans
buttom line, this is not the way to override the framework endpoints. you can simply create a controller with mappings for these endpoints (e.g /oauth/authorize or /oauth/token). automatically these mappings will get precedence over framework endpoints. for more info refer to spring doc

in SpringBoot 2.x(2.1.0) the custom filter process once but produce duplicated data

I'm new in using SpringBoot 2.1.0 with JSP (with some reasons) to develop web applications.
I'm using a filter to save access info into the database, which are mapping to one type of url.
But there're some problems:
1. When I click the link on a menu, the page is new opened in the browser, but logs output twice, it's indicate that doFilterInternal method executed twice, this situation is NOT correct;
2018-12-13 13:43:07.405 WARN 14912 --- [nio-8096-exec-2] c.y.l.c.filters.rpt.AccessMenuFilter : ---------------------------- Access Once ----------------------------------------
2018-12-13 13:43:07.405 WARN 14912 --- [nio-8096-exec-3] c.y.l.c.filters.rpt.AccessMenuFilter : ---------------------------- Access Once ----------------------------------------
2. Then I right click the mouse on opened page in step one, and choose refresh the iframe, logs output only once, it's indicate that doFilterInternal method executed once, this situation is correct, in step one it should execute once too.
2018-12-13 13:44:02.118 WARN 14912 --- [nio-8096-exec-1] c.y.l.c.filters.rpt.AccessMenuFilter : ---------------------------- Access Once ----------------------------------------
insert two records into Database in step one, one record in step two
The filter extends to OncePerRequestFilter, see from other posts, it may causes the call twice, but why in step 2 the filter call once.
I post the main codes below:
POM.xml
https://github.com/richard20427176/pom-config/blob/master/pom.xml
Below is main of SpringBootConfig code:
#SpringBootConfiguration
public class SpringBootConfig implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
// configurer.setUseTrailingSlashMatch(false);
configurer.setUseRegisteredSuffixPatternMatch(true);
}
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true)
.favorParameter(true)
.parameterName("format")
.ignoreAcceptHeader(true)
.defaultContentType(MediaType.TEXT_HTML)
.mediaType("html", MediaType.TEXT_HTML)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xls", MediaType.valueOf("application/vnd.ms-excel"))
.mediaType("xlsx", MediaType.valueOf("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
}
#Override
public void configureViewResolvers(ViewResolverRegistry registry) {
Set<String> modelKeys=new HashSet<>();
modelKeys.add("list");
modelKeys.add("table");
registry.jsp("/views/", ".jsp");
registry.enableContentNegotiation(new MappingJackson2JsonView());
XlsView xlsView=new XlsView();
xlsView.setModelKeys(modelKeys);
registry.enableContentNegotiation(xlsView);
XlsxView xlsxView=new XlsxView();
xlsxView.setModelKeys(modelKeys);
registry.enableContentNegotiation(xlsxView);
}
}
And below is Filter Config code:
#Configuration
public class FilterConfig implements WebMvcConfigurer {
#Bean
public FilterRegistrationBean shiroDelegatingFilterProxy() {
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
}
The last, below is implement of the Filter code:
#Component
public class AccessMenuFilter extends OncePerRequestFilter {
private static final Logger LOGGER= LoggerFactory.getLogger(AccessMenuFilter.class);
#Autowired
private MonitorService monitorService;
#Autowired
private MenuService menuService;
private Set<MenuIsMonitorVo> monitorMenus=new HashSet<>();
private Map<String, RequestMatcher> menuRequestMatcherMap=new ConcurrentHashMap<>();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String pageNumber = request.getParameter(Page.PAGE_NUMBER_REQUEST_PARAM_NAME);
// If page no greater than 2, then skip
if(StringUtils.isBlank(pageNumber) || pageNumber.compareTo("1")<=0) {
for(Map.Entry<String,RequestMatcher> entry:menuRequestMatcherMap.entrySet()) {
if (entry.getValue().matches(request)) {
String username = ShiroBaseService.getLoginUser().getUsername();
UserAgent userAgent=UserAgent.parseUserAgentString(request.getHeader(HttpHeaders.USER_AGENT));
String browser= WebUtil.getBrowserName(userAgent);
CreateAccessMenuDto createAccessMenuDto = new CreateAccessMenuDto();
createAccessMenuDto.setMenuId(entry.getKey());
createAccessMenuDto.setUserName(username);
createAccessMenuDto.setOsName(userAgent.getOperatingSystem().getName());
createAccessMenuDto.setBrowserName(browser);
createAccessMenuDto.setIpAddress(RemoteIpHelper.getRemoteIpFrom(request));
createAccessMenuDto.setRequestLocale(request.getLocale().getDisplayName());
createAccessMenuDto.setCreateTime(new Date());
monitorService.asyncCreateAccessMenu(createAccessMenuDto);
LOGGER.warn("---------------------------- Access Once ----------------------------------------");
LOGGER.debug("Successfully add user access log:[SessionId:{};Username:{};platform:{};Browser:{};IPAddress:{};MenuId:{}]. The request url is {}",
request.getSession(false).getId(),
username,
userAgent.getOperatingSystem().getName(),
browser,
RemoteIpHelper.getRemoteIpFrom(request),
entry.getKey(),
request.getRequestURL());
break;
}
}
}
} catch (Exception ex) {
LOGGER.error("User Access fail due to the reason:"+ex.getMessage());
} finally {
filterChain.doFilter(request,response);
}
}
#Override
protected void initFilterBean() throws ServletException {
if (monitorMenus != null && monitorMenus.size() > 0) {
RequestMatcher matcher;
for (MenuIsMonitorVo menu : monitorMenus) {
if (menu.getIsMonitor().equals("1")) {
String pattern = menu.getMenuUrl();
if (!pattern.startsWith("/")) {
pattern = "/" + pattern;
}
if (pattern.indexOf("?") != -1) {
pattern = pattern.substring(0, pattern.indexOf("?"));
}
LOGGER.info("Add menu[MenuId:{},pattern:{}] to access log monitor candidate map.", menu.getMenuId(), pattern);
matcher = new AntPathRequestMatcher(pattern);
menuRequestMatcherMap.put(menu.getMenuId(), matcher);
}
}
} else {
monitorMenus = menuService.menuIsMonitor().stream().collect(Collectors.toSet());
}
}
}
I hope anyone can help me and thanks very much.
I'm quite sure that the OPTIONS requests are doing those extra filter invocations for you.
Please check http://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS for more information. It should be visible in the network tab of your browser.

Own HttpTraceRepository

i created my own HttpTraceRepository, for write and read httpTrace from mongodb on capped collection.
Today, i don't understand why HttpTrace is a final class without public constructor.
In my case, when i read mongodb ( findAll method ) to find the last X hours http trace and show it in spring-boot admin UI, it's impossible to instanciante HttpTrace object.
#Component
public class MongoTraceRepository implements HttpTraceRepository {
private static final String ADMIN_TRACE = "admin.trace";
private MongoOperations mongoOps;
#Value("${admin.display.trace.last.x.hours}")
private int displayTraceLastXHours;
#Autowired
public MongoTraceRepository(MongoOperations mongoOps) {
this.mongoOps = mongoOps;
}
#PostConstruct
public void initialize() {
MongoCollection<Document> collection = mongoOps.getCollection(ADMIN_TRACE);
boolean collectionExists = mongoOps.collectionExists(ADMIN_TRACE);
if (!collectionExists) {
collection.drop();
// 100 Mo max, 500 000 documents max, capped !
mongoOps.createCollection(ADMIN_TRACE, CollectionOptions.empty().size(104857600).maxDocuments(500000).capped());
}
}
#Override
public List<HttpTrace> findAll() {
Date yesterday = Date.from(LocalDateTime.now().minusHours(displayTraceLastXHours).atZone(ZoneId.systemDefault()).toInstant());
return mongoOps.find(new Query(where("timestamp").gte(yesterday)), HttpTrace.class, ADMIN_TRACE)
.stream()
.sorted((o1, o2) -> o2.getTimestamp().compareTo(o1.getTimestamp()))
.collect(Collectors.toList());
}
#Override
#Async
public void add(HttpTrace traceInfo) {
mongoOps.save(traceInfo, ADMIN_TRACE);
}
}
This is the error log :
org.springframework.data.mapping.MappingException: No property request found on entity class org.springframework.boot.actuate.trace.http.HttpTrace$Request to bind constructor parameter to!
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:68)
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:49)
at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:75)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:86)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:273)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:253)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1387)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1334)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1276)
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:71)
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:49)
at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:75)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:86)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:273)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:253)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:202)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:198)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:86)
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:2785)
at org.springframework.data.mongodb.core.MongoTemplate.executeFindMultiInternal(MongoTemplate.java:2448)
at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:2244)
at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:2227)
at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:770)
There is a constructor especially suited to create HTTPTrace from a persistence store.
It was added since 2.1.0 though.

Looking for a solution to extend Spring MVC with another Component/Annotation

Suppose I have a Website that is used in normal mode (browser) and in some other mode, like a MobileView mode (inside a mobile app). For each Controller I create, there might be correspondent controller for MobileView, processing the same url.
The easiest solution is to create ifs in all the Controllers that have MobileView logic. Another solution would be to use a correspondent url for MobileView (similar to the normal url) and two separate Controllers (possible where one extends from another; or use some other way to recycle common code)
But, a more elegant solution would be to have some extra annotations, like #SupportsMobileView (to mark a controller, and tell the app that this will have a correspondent MobileView Controller) and #MobileViewController (to mark a second controller, and tell the app that this controller needs to run immediately after the initial controller marked with #SupportsMobileView). The link between a normal controller and a MobileView controller would be through the url they process (defined with #RequestMapping).
Is it possible to extend Spring MVC (A)? Where to inject new annotation scanners (B) and annotation handlers / component handlers (C)? How should the MobileView controller be executed (D) (right now I am thinking that it could be executed through AOP, where the new handler of my new controller type programatically creates a Join-Point on the corresponding normal controller)
Note that I did not mention how this MobileView mode is triggered and detected. Let's just say that there a Session boolean variable (flag) for that.
Critics on any points (A), (B), (C) or (D) are welcomed, as well as technical hints and alternative solution to any point or the whole solution.
HandlerInterceptor can be used to intercept the RequestMapping handling. This is a simple example how to configure and implement one.
You can check for your session variable and will have a bunch of methods that will allow you to do custom processing or just exchange the view from the normal controller handling with your mobile view.
Ok, warnings:
this is only a proof of concept of what I understood must be done so:
+#MobileViewEnable and #MobileView annotated (and related) methods need to stay in the same controller
+there's no check for the httpAction used
+the two methods must have the same signature
+mobileView annotation value and requestMapping annotation value must be equals and uniques
+the logic inside callYourLogic(..) defines which method is going to be called, at the moment there's a very simple logic that check if exist the parameter ("mobile") in the request, just to test
+this code is not intended to be used as is (at all)
+don't know if it works at all outside my pc (joke :D, ehm..)
SO:
Annotations:
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileView {
String value() default "";
}
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileViewEnable {
}
ExampleController:
#Controller
public class MainController extends BaseController {
private final static Logger logger = LoggerFactory.getLogger(MainController.class);
private final static String PROVA_ROUTE = "prova";
#MobileViewEnable
#RequestMapping(PROVA_ROUTE)
public String prova() {
logger.debug("inside prova!!!");
return "provaview";
}
#MobileView(PROVA_ROUTE)
public String prova2() {
logger.debug("inside prova2!!!");
return "prova2view";
}
}
Aspect definition:
<bean id="viewAspect" class="xxx.yyy.ViewAspect" />
<aop:config>
<aop:pointcut expression="#annotation(xxx.yyy.MobileViewEnable)" id="viewAspectPointcut" />
<aop:aspect ref="viewAspect" order="1">
<aop:around method="around" pointcut-ref="viewAspectPointcut" arg-names="viewAspectPointcut"/>
</aop:aspect>
</aop:config>
Aspect implementation:
public class ViewAspect implements BeforeAdvice, ApplicationContextAware {
private final static Logger logger = LoggerFactory.getLogger(ViewAspect.class);
private ApplicationContext applicationContext;
public Object around(ProceedingJoinPoint joinPoint) {
Method mobileViewAnnotatedMethod = null;
HttpServletRequest request = getCurrentHttpRequest();
String controllerName = getSimpleClassNameWithFirstLetterLowercase(joinPoint);
Object[] interceptedMethodArgs = getInterceptedMethodArgs(joinPoint);
String methodName = getCurrentMethodName(joinPoint);
Method[] methods = getAllControllerMethods(joinPoint);
Method interceptedMethod = getInterceptedMethod(methods, methodName);
String interceptedMethodRoute = getRouteFromInterceptedMethod(interceptedMethod);
if (callYourLogic(request)) {
mobileViewAnnotatedMethod = getMobileViewAnnotatedMethodWithRouteName(methods, interceptedMethodRoute);
if (mobileViewAnnotatedMethod != null)
return invokeMethod(mobileViewAnnotatedMethod, interceptedMethodArgs, controllerName);
}
return continueInterceptedMethodExecution(joinPoint, interceptedMethodArgs);
}
private Object continueInterceptedMethodExecution(ProceedingJoinPoint joinPoint, Object[] interceptedMethodArgs) {
try {
return joinPoint.proceed(interceptedMethodArgs);
} catch (Throwable e) {
logger.error("unable to proceed with intercepted method call: " + e);
}
return null;
}
private Object[] getInterceptedMethodArgs(JoinPoint joinPoint) {
return joinPoint.getArgs();
}
private boolean callYourLogic(HttpServletRequest request) {
// INSERT HERE YOUR CUSTOM LOGIC (e.g.: is the server accessed from a mobile device?)
// THIS IS A STUPID LOGIC USED ONLY FOR EXAMPLE
return request.getParameter("mobile")!= null;
}
private HttpServletRequest getCurrentHttpRequest() {
return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
}
private String invokeMethod(Method method, Object[] methodArgs, String className) {
if (method != null) {
try {
Object classInstance = getInstanceOfClass(method, className);
return (String) method.invoke(classInstance, methodArgs);
} catch (Exception e) {
logger.error("unable to invoke method" + method + " - " + e);
}
}
return null;
}
private Object getInstanceOfClass(Method method, String className) {
return applicationContext.getBean(className);
}
private Method getMobileViewAnnotatedMethodWithRouteName(Method[] methods, String routeName) {
for (Method m : methods) {
MobileView mobileViewAnnotation = m.getAnnotation(MobileView.class);
if (mobileViewAnnotation != null && mobileViewAnnotation.value().equals(routeName))
return m;
}
return null;
}
private String getRouteFromInterceptedMethod(Method method) {
RequestMapping requestMappingAnnotation = method.getAnnotation(RequestMapping.class);
if (requestMappingAnnotation != null)
return requestMappingAnnotation.value()[0];
return null;
}
private String getCurrentMethodName(JoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
private Method[] getAllControllerMethods(JoinPoint joinPoint) {
return joinPoint.getThis().getClass().getSuperclass().getMethods();
}
private String getSimpleClassNameWithFirstLetterLowercase(JoinPoint joinPoint) {
String simpleClassName = joinPoint.getThis().getClass().getSuperclass().getSimpleName();
return setFirstLetterLowercase(simpleClassName);
}
private String setFirstLetterLowercase(String simpleClassName) {
String firstLetterOfTheString = simpleClassName.substring(0, 1).toLowerCase();
String restOfTheString = simpleClassName.substring(1);
return firstLetterOfTheString + restOfTheString;
}
private Method getInterceptedMethod(Method[] methods, String lookingForMethodName) {
for (Method m : methods)
if (m.getName().equals(lookingForMethodName))
return m;
return null;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

Resources