Search code examples
spring-bootjavafx

Cannot invoke "com.example.service.PersonService.save(Object)" because "this.personService" is null


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)

Solution

  • 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.

    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).