Search code examples
spring-bootspring-webflowspring-webflow-2

Spring WebFlow (seemingly) randomly stops working in Spring Boot app


I have a flow which seemed to be working fine until yesterday, when suddenly I started getting the following exception in my HTML page that maps to the first state in my flow:

org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'flowScope' cannot be found on null

The offending line of code was:

<h3 th:text="${flowRequestContext.flowScope}"/>

Further investigation showed that none of the flow variables are available anymore. Furthermore if I put print statements into the Service which the flow makes various calls to, I can see that none of these methods are being called anymore - it's like the flow just isn't running at all.

This was working fine previously. I even reverted all of my local changes to a previously stable version of the code, and the same issue was happening there as well. The only thing that seemed to temporarily get around the problem was to restart my computer - the problem disappeared for a short while but then came back.

To be honest I'm completely out of ideas as to what could have started causing such an intermittent problem. I was thinking along the lines of a stale Java process running in the background interfering with future runs of the application, but have checked for and killed off any remaining process in between deploys to no avail.

I have included what I hope are the relevant file below. Any help resolving this issue would be very much appreciated.

checkout.xml

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow
            http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <on-start>
        <set name="flowScope.paymentMethods" value="checkoutWidgetService.getPaymentMethods()"/>
        <set name="flowScope.deliveryAddress" value="checkoutWidgetService.getDeliveryAddress()"/>
        <set name="flowScope.sessionId" value="externalContext.nativeRequest.session.id"/>
    </on-start>

    <view-state id="payment-methods" view="payment-methods">
        <transition on="selectPaymentMethod" to="new-details">
            <evaluate expression="checkoutWidgetService.getCardDetails(requestParameters.type)" result="flowScope.cardDetails"/>
        </transition>
    </view-state>

    <view-state id="new-details" view="new-details">
        <transition on="submitDetails" to="summary">
            <evaluate expression="checkoutWidgetService.buildCardDetails(requestParameters)" result="flowScope.cardDetails"/>
        </transition>
    </view-state>

    <view-state id="summary" view="summary">
        <transition on="completeCheckout" to="redirect">
            <evaluate expression="checkoutWidgetService.completeCheckout(externalContext.nativeRequest.session, flowRequestContext, flowScope.cardDetails)"/>
        </transition>
        <transition on="cancelCheckout" to="redirect">
            <evaluate expression="checkoutWidgetService.cancelCheckout(externalContext.nativeRequest.session, flowRequestContext)"/>
        </transition>
    </view-state>

    <end-state id="redirect" view="externalRedirect:contextRelative:/payments/checkout-widgets/end"/>
</flow>

WebflowConfig.java

@Configuration
@AutoConfigureAfter(MvcConfig.class)
public class WebflowConfig extends AbstractFlowConfiguration {

    @Autowired
    private SpringTemplateEngine templateEngine;

    @Bean
    public FlowExecutor flowExecutor() {
        return getFlowExecutorBuilder(flowRegistry())
                .addFlowExecutionListener(new SecurityFlowExecutionListener())
                .build();
    }

    @Bean
    public FlowDefinitionRegistry flowRegistry() {
        return getFlowDefinitionRegistryBuilder(flowBuilderServices())
                .addFlowLocation("classpath:/templates/checkout.xml", "payments/checkout-widget/start")
                .build();
    }

    @Bean
    public FlowBuilderServices flowBuilderServices() {
        return getFlowBuilderServicesBuilder()
                .setViewFactoryCreator(mvcViewFactoryCreator())
                .setDevelopmentMode(true)
                .build();
    }

    @Bean
    public FlowController flowController() {
        FlowController flowController = new FlowController();
        flowController.setFlowExecutor(flowExecutor());
        return flowController;
    }

    @Bean
    public FlowHandlerMapping flowHandlerMapping() {
        FlowHandlerMapping flowHandlerMapping = new FlowHandlerMapping();
        flowHandlerMapping.setFlowRegistry(flowRegistry());
        flowHandlerMapping.setOrder(-1);
        return flowHandlerMapping;
    }

    @Bean
    public FlowHandlerAdapter flowHandlerAdapter() {
        FlowHandlerAdapter flowHandlerAdapter = new FlowHandlerAdapter();
        flowHandlerAdapter.setFlowExecutor(flowExecutor());
        flowHandlerAdapter.setSaveOutputToFlashScopeOnRedirect(true);
        return flowHandlerAdapter;
    }

    @Bean
    public AjaxThymeleafViewResolver thymeleafViewResolver() {
        AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
        viewResolver.setViewClass(FlowAjaxThymeleafView.class);
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }

    @Bean
    public MvcViewFactoryCreator mvcViewFactoryCreator() {
        List<ViewResolver> viewResolvers = new ArrayList<>();
        viewResolvers.add(thymeleafViewResolver());

        MvcViewFactoryCreator mvcViewFactoryCreator = new MvcViewFactoryCreator();
        mvcViewFactoryCreator.setViewResolvers(viewResolvers);
        mvcViewFactoryCreator.setUseSpringBeanBinding(true);
        return mvcViewFactoryCreator;
    }

}

CheckoutWidgetSessionMvcController.java

@Controller
@RequestMapping("/payments/checkout-widgets")
public class CheckoutWidgetSessionMvcController {

    @Inject
    private CheckoutWidgetService service;

    @RequestMapping(value = {"/start"}, method = RequestMethod.GET)
    public ModelAndView paymentMethods() {
        return new ModelAndView("payment-methods", null);
    }

    @RequestMapping(value = "/end", method = RequestMethod.GET)
    public String invalidateSession(HttpSession session) {
        service.invalidateSession(session);
        return "dummy-redirect-post";
    }
}

CheckoutWidgetService.java

public interface CheckoutWidgetService {

    List<PaymentMethod> getPaymentMethods();

    CardDetails getCardDetails(String name);

    CardDetails buildCardDetails(LocalParameterMap params);

    String getDeliveryAddress();

    void completeCheckout(HttpSession session, RequestContext context, CardDetails cardDetails);

    void cancelCheckout(HttpSession session, RequestContext context);

    void invalidateSession(HttpSession session);
}

CheckoutWidgetServiceImpl.java

@Service("checkoutWidgetService")
public class CheckoutWidgetServiceImpl implements CheckoutWidgetService {

    @Inject
    private CheckoutWidgetSessionService sessionService;

    private final List<PaymentMethod> paymentMethods = new ArrayList<>();

    private final String deliveryAddress;

    public CheckoutWidgetServiceImpl() {
        paymentMethods.add(new PaymentMethod("PayPal", "/images/paypal-logo.png")); 
        paymentMethods.add(new PaymentMethod("Mastercard", "/images/mc-logo.png"));
        paymentMethods.add(new PaymentMethod("Visa", "/images/visa-logo.png"));
        paymentMethods.add(new PaymentMethod("Amex", "/images/amex-logo.png"));
        paymentMethods.add(new PaymentMethod("Google Checkout", "/images/google-logo.png"));
        deliveryAddress = "xxxxx";
    }
    @Override
    public List<PaymentMethod> getPaymentMethods() {
        System.out.println("Returning paymentMethods: " + paymentMethods);
        return paymentMethods;
    }

    @Override
    public CardDetails getCardDetails(String name) {
        CardDetails cardDetails = new CardDetails();
        cardDetails.setCardType(name);
        return cardDetails;
    }

    @Override
    public CardDetails buildCardDetails(LocalParameterMap params) {
        CardDetails cardDetails = new CardDetails();
        cardDetails.setCardNumber(params.get("cardNumber"));
        cardDetails.setExpiryMonth(params.get("expiryMonth"));
        cardDetails.setExpiryYear(params.get("expiryYear"));
        cardDetails.setNameOnCard(params.get("nameOnCard"));
        cardDetails.setCvv2(params.get("cvv2"));
        return cardDetails;
    }

    @Override
    public String getDeliveryAddress() {
        return deliveryAddress;
    }

    @Override
    public void invalidateSession(HttpSession session) {
        session.invalidate();
    }

    private RedirectUrls getRedirectUrls(String sessionId) {
        CheckoutWidgetSession widgetSession = sessionService.getCheckoutWidgetSession(sessionId).get();
        return widgetSession.getRedirectUrls();
    }

    @Override
    public void completeCheckout(HttpSession session, RequestContext context, CardDetails cardDetails) {
        RedirectUrls redirects = getRedirectUrls(session.getId());

        context.getFlowScope().remove("paymentMethods");

        UriBuilder uriBuilder = UriBuilder.fromUri(URI.create(redirects.getSuccessUrl()));
        String forwardUrl = uriBuilder.queryParam("transactionId", "12345").toString();
        context.getFlowScope().put("forwardUrl", forwardUrl);
        context.getFlowScope().put("target", "_top");
    }

    @Override
    public void cancelCheckout(HttpSession session, RequestContext context) {
        RedirectUrls redirects = getRedirectUrls(session.getId());

        context.getFlowScope().remove("paymentMethods");

        String forwardUrl = redirects.getCancelUrl();
        context.getFlowScope().put("forwardUrl", forwardUrl);
        context.getFlowScope().put("target", "_top");
    }
}

Application.java

@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.pay.widgets.checkout"})
@Import(JerseyAutoConfiguration.class)
public class Application extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);

    }
}

Solution

  • Okay this was a really stupid mistake on my part, the problem turned out to be down to a typo in the @RequestParameter annotation on the Controller:

    payments/checkout-widgets
    

    Which didn't line up with what was in the WebflowConfig defined flowRegistry:

    payments/checkout-widget
    

    I can only assume the resource was cached by Tomcat which is why it took so long for the issue to manifest and threw me off the scent in terms of suspecting my own changes to be responsible.