Spring Boot – How to Fix ‘Cannot invoke \”com.example.service.PersonService.save(Object)\” because \”this.personService\” is null’ Error in JavaFX

javafxspring-boot

I have a personal program that uses Springboot and Javafx. I've been trying to gain more skills in creating springboot and javafx apps. Program launches an fxml page where a person can update a person's records, clicks save, and is then saved to an mssql database.

I'm trying to save a new person record to a mssql database.

PersonEntity.java

@Entity
@Getter
@NoArgsConstructor(force = true)
@Data
@Table(name = "persons")
public class PersonEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "personID", nullable = false)
    private final Integer personID;

    @Column(name = "first_name")
    @Setter
    private String firstName;
    @Column(name = "last_name")
    @Setter
    private String lastName;
    @Column(name = "dob")
    @Setter
    private String dob;

PersonRepo.java

@Repository
public interface PersonRepo extends JpaRepository<PersonEntity, String> {
    List<PersonEntity> findAll();
}

PersonService.java

public interface PersonService{
    Optional<PersonEntity> save(PersonEntity personEntity);
    PersonEntity update(PersonEntity personEntity);
    PersonEntity findAll();
   
}

PersonServiceImpl.java


@Service
@RequiredArgsConstructor
public class PersonServiceImplimplements PersonService {
    @Autowired
    private PersonRepo repo;

    @Override
    public Optional<PersonEntity> save(PersonEntity personEntity) {
        return Optional.of(repo.save(personEntity));
    }

    @Override
    public PersonEntity update(PersonEntity personEntity){ return repo.save(personEntity);}

    @Override
    public PersonEntity findAll() {
        return (PersonEntity ) repo.findAll();
    }
}

PersonController.java

@Component
public class PersonController {
 @Autowired
 PersonService personService;
 public TextField lName;
 public TextField fName;
 public TextField dob;
 public Button onSaveNewPerson;

   @FXML
    public void onSaveNewPerson(ActionEvent event) throws SQLException {
        if(fName != null){
            PersonEntity newPerson = new PersonEntity ();
            fName.setText(newPerson.getFirstName());
            personService.save(newPerson);

        }
}

PersonsRecord.fxml

<AnchorPane fx:id="scenePane" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.controllers.PersonController">
    <Pane layoutX="-2.0" layoutY="91.0" prefHeight="800.0" prefWidth="1004.0">
        <Label layoutX="14.0" layoutY="14.0" text="New Person Form">
           <font>
               <Font size="20.0"/>
           </font>
        </Label>
        <Label layoutX="14.0" layoutY="69.0" text="First Name"/>
        <Label layoutX="16.0" layoutY="130.0" text="Last name"/>
        <Label layoutX="18.0" layoutY="130.0" text="Date of Birth"/>
        <Label layoutX="20.0" layoutY="130.0" text="Address"/>
        <TextField fx:id="fName" layoutX="98.0" layoutY="66.0"/>
        <TextField fx:id="lName" layoutX="98.0" layoutY="126.0"/>
        <TextField fx:id="dob" layoutX="98.0" layoutY="146.0"/>
        <TextField fx:id="personAddress" layoutX="98.0" layoutY="166.0"/>
        <Button fx:id="saveNewPerson" layoutX="803.0" layoutY="333.0" mnemonicParsing="false"
                onAction="#onSaveNewPerson" prefHeight="58.0" prefWidth="187.0" text="Save New Person"/>
        <Label layoutX="597.0" layoutY="70.0" text="DOB"/>
        <TextField fx:id="dob" layoutX="691.0" layoutY="66.0"/>
    </Pane>
</AnchorPane>

Currently after clicking the onSaveNewPersonbutton I receive the following error –

Cannot invoke "com.example.services.PersonService.save(com.example.entity.persons.PersonEntity)" because "this.personService" is null

I do want all the textfields to save but for right now I've just been testing with the fname field.

I've ensured the @Autowired is set for the personService in the controller. I also made sure the @Repository is set for the PersonRepo.

I also added the implements CommandLineRunner to the FxApplication file and had a test person added to the database to ensure proper connectivity. That ran just fine and a person was entered successfully. Code for investigation –

@SpringBootApplication
@EnableJpaRepositories("com.example.repo")
public class FxApplication extends Application implements CommandLineRunner {
    private ConfigurableApplicationContext springContext;
    private Parent root;
    @Autowired
    PersonRepo personRepo;
    public static void main(String[] args){
        launch(FxApplication.class, args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setControllerFactory(springContext::getBean);
        fxmlLoader.setLocation(getClass().getResource("/PersonsRecord.fxml"));
      
        Parent root = fxmlLoader.load();
        stage.setTitle("Add NewPerson");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    @Override
    public void init() throws Exception{
      springContext = SpringApplication.run(FxApplication.class);

    }

    @Override
    public void stop() throws Exception{
        springContext.close();
        Platform.exit();
    }

    @Override
    public void run(String... args) throws Exception {
        PersonEntity person = new PersonEntity ();
        person.setFirstName("TestSpring3");
        person.setLastName("LastNameTest4");
        person.setDOB("1/1/2000")
        personRepo.save(person);

    }
}

I edited my code and used the template that was provided via @David Weber.

This is now my FxApplicationLauncher.java class that I have edited.

package com.example;

import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.*;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.stereotype.Component;

import java.io.IOException;

@SpringBootApplication
@EnableJpaRepositories("com.example.repo")
public class FxApplicationLauncher {

    public static void main(String[] args) {
        Application.launch(FxApplication.class, args);
    }
    public static class FxApplication extends Application {
        private ConfigurableApplicationContext context;
        @Override
        public void init(){
            ApplicationContextInitializer<GenericApplicationContext> initializer = applicationContext -> {
                applicationContext.registerBean(Application.class, () -> FxApplication.this);
                applicationContext.registerBean(Parameters.class, this::getParameters);
                applicationContext.registerBean(HostServices.class, this::getHostServices);
            };

            this.context = new SpringApplicationBuilder()
                    .sources(FxApplicationLauncher.class)
                    .initializers(initializer)
                    .run(getParameters().getRaw().toArray(new String[0]));
        }
        @Override
        public void start(Stage stage)  { this.context.publishEvent(new StageIsReadyEvent(stage)); }

        @Override
        public void stop() throws Exception {
            context.close();
            Platform.exit();
            System.exit(0);
        }

        @Component
        public static class FxApplicationStageIsReadyListener implements ApplicationListener<StageIsReadyEvent>{
            @Value("${spring.application.name}")
            private  String applicationTitle;
            private ApplicationContext applicationContext;

            public void JfxSpringBootStageIsReadyListener(
                    @Value("${spring.application.name}") String applicationTitle,
                    ApplicationContext applicationContext) {

                this.applicationTitle = applicationTitle;
                this.applicationContext = applicationContext;
        }

            @Override
            public void onApplicationEvent(StageIsReadyEvent event) {
                try {
                    FXMLLoader fxmlLoader = new FXMLLoader();
                    fxmlLoader.setLocation(getClass().getResource("/PersonsRecord.fxml"));
                    fxmlLoader.setControllerFactory(applicationContext::getBean);                
                    Parent root = fxmlLoader.load();

                    Scene scene = new Scene(root, 600, 600);
                    Stage stage = event.getStage();               
                    stage.setScene(scene);
                    stage.setTitle(this.applicationTitle);
                    stage.show();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

        public static class StageIsReadyEvent extends ApplicationEvent {
            public Stage getStage() {
                return (Stage) getSource();
            }

            public StageIsReadyEvent(Stage source) {
                super(source);
            }
        }

    }
}

For some reason the applicationContext is null.

java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:208)
at com.example.gaitlabapp.FxApplicationLauncher$FxApplication$FxApplicationStageIsReadyListener.onApplicationEvent(FxApplicationLauncher.java:84)
at com.example.gaitlabapp.FxApplicationLauncher$FxApplication$FxApplicationStageIsReadyListener.onApplicationEvent(FxApplicationLauncher.java:65)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:185)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:178)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:156)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:451)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:384)
at com.example.gaitlabapp.FxApplicationLauncher$FxApplication.start(FxApplicationLauncher.java:51)
at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:847)
at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:484)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:842)

Best Answer

Explanation:

I am quite sure, that your service is null because your controller is not a Spring Component. You marked it with @Component, which is a nice idea, but because you use FXML I bet, that your controller is not created by Spring Boot, but by the FxmlLoader. This leads to "ignoring" the annotation, because the FxmlLoader creates a new instance of your controller by using the keyword new.

Example repos of mine which demonstrate the use of Spring Boot and JavaFx with Maven and Gradle. I would appreciate a star for supporting free open source:

JFX with Spring Boot and Gradle (better and more up to date)

JFX with Spring Boot and Maven

What to do next:

You have several options from here.

  1. Easiest: Download the template and alter it to your needs. Everything is preconfigured and runs out of the box.
  2. Mid workaround: Assign your Spring Boot Application context to a static variable and get your service in a non Spring Component by using Service service = springContext.getBean(Service.class).
  3. Hardest: Restructure your code so your controller is a Spring Component (maybe build your own SpringFxmlLoader class).

Additional information:

Be aware when using Spring components. They can be stateless or stateful. In easier words: They can be a Singleton (scope singleton) or an own instance for every injection (scope prototype).

Related Question