jscherman
jscherman

Reputation: 6189

Use Spring @Value on non-component object

I've had this issue that i didn't know how to resolve. I made my Restful API using Spring Boot, and i am implementing the DTO-Domain-Entity pattern, so on this particular case i have this controller's method

@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<UserResponseDTO> createUser(@RequestBody UserRequestDTO data) {
    UserDomain user = this.mapper.map(data, UserDomain.class);
    UserDomain createdUser = this.service.createUser(user);
    UserResponseDTO createdUserDTO = this.mapper.map(createdUser, UserResponseDTO.class);
    return new ResponseEntity<UserResponseDTO>(createdUserDTO, HttpStatus.CREATED);
}

public class UserDomain {

    private Long id;

    private Date createdDate;

    private Date updatedDate;

    private String username;

    private String password;

    @Value("${default.user.enabled:true}") // I have default-values.properties being loaded in another configuration file
    private Boolean enabled;
}

I am transforming UserRequestDTO object to UserDomain. As i understand, UserRequestDTO is a bean that is being injected. Then i am transforming this to UserDomain, the problem here is that UserDomain object is not a component, so enabled attribute will not take the default value.

In the case i wouldn't want to handle UserDomain as a bean, how could i make spring to load default values (just enabled attribute in this case)?


EDIT

It's not the same answer, since my goal is get it done using @Value annotations.

Anyways, Would it be a better way doing something like this instead Constantine suggested?

public class UserDomain {

    @Autowired
    private Environment environment;

    private Boolean enabled;

    UserDomain(){
         this.enabled = environment.getProperty("default.user.enabled");
         // and all the other ones
    }

}

Upvotes: 19

Views: 25024

Answers (2)

Constantine
Constantine

Reputation: 3257

If your mapper has a method that takes already prepared instance instead of Class, then you can add the prototype-scoped UserDomain bean and call context.getBean() from the controller method.

Controller

...

@Autowired
private WebApplicationContext context;

@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<UserResponseDTO> createUser(@RequestBody UserRequestDTO data) {
    UserDomain user = this.mapper.map(data, getUserDomain());
    UserDomain createdUser = this.service.createUser(user);
    UserResponseDTO createdUserDTO = this.mapper.map(createdUser, UserResponseDTO.class);
    return new ResponseEntity<UserResponseDTO>(createdUserDTO, HttpStatus.CREATED);
}

private UserDomain getUserDomain() {
    return context.getBean(UserDomain.class);
}

...

Spring configuration

@Configuration
public class Config {

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer propConfigurer = new PropertySourcesPlaceholderConfigurer();
        propConfigurer.setLocation(new ClassPathResource("application.properties"));
        return propConfigurer;
    }

    @Bean
    @Scope("prototype")
    public UserDomain userDomain() {
        return new UserDomain();
    }

    ...
}

Otherwise, you can use @Configurable and AspectJ compile-time weaving. But you have to decide if it is worth to introduce weaving in your project, since you have other ways to handle the situation.

pom.xml

...

<!-- additional dependencies -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.6</version>
</dependency>

...

<!-- enable compile-time weaving with aspectj-maven-plugin -->
<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>1.7</version>
            <configuration>
                <complianceLevel>1.8</complianceLevel>
                <encoding>UTF-8</encoding>
                <aspectLibraries>
                    <aspectLibrary>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-aspects</artifactId>
                    </aspectLibrary>
                </aspectLibraries>
                <Xlint>warning</Xlint>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

...

UserDomain.java

@Configurable
public class UserDomain {

    private Long id;

    private Date createdDate;

    private Date updatedDate;

    private String username;

    private String password;

    @Value("${default.user.enabled:true}")
    private Boolean enabled;

    ...
}

Spring configuration

@EnableSpringConfigured is the same as <context:spring-configured>.

@Configuration
@EnableSpringConfigured
public class Config {

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer propConfigurer = new PropertySourcesPlaceholderConfigurer();
        propConfigurer.setLocation(new ClassPathResource("application.properties"));
        return propConfigurer;
    }

    ...
}

Please consult Spring documentation for more information on AspectJ and @Configurable.


EDIT

Regarding your edit.

Please note that you use @Autowired there. It means that UserDomain instances have to be managed by the Spring container. The container is not aware about instances created outside of it, so @Autowired (exactly as @Value) will not be resolved for such instances, e.g. UserDomain userDomain = new UserDomain() or UserDomain.class.newInstance(). Thus, you still have to add a prototype-scoped UserDomain bean to your context. Effectively, it means that the proposed approach is similar to the @Value-associated approach, except that it ties your UserDomain to Spring Environment. Therefore, it is bad.

It is still possible to craft a better solution using Environment and ApplicationContextAware without tying your domain objects to Spring.

ApplicationContextProvider.java

public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public static <T> T getEnvironmentProperty(String key, Class<T> targetClass, T defaultValue) {
        if (key == null || targetClass == null) {
            throw new NullPointerException();
        }

        T value = null;
        if (applicationContext != null) {
            System.out.println(applicationContext.getEnvironment().getProperty(key));
            value = applicationContext.getEnvironment().getProperty(key, targetClass, defaultValue);
        }
        return value;
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

UserDomain.java

public class UserDomain {

    private Boolean enabled;

    public UserDomain() {
         this.enabled = ApplicationContextProvider.getEnvironmentProperty("default.user.enabled", Boolean.class, false);
    }

    ...
}

Spring configuration

@Configuration
@PropertySource("classpath:application.properties")
public class Config {

    @Bean
    public ApplicationContextProvider applicationContextProvider() {
        return new ApplicationContextProvider();
    }

    ...
}

However, I do not like the additional complexity and sloppiness of this approach. I think it is not justified at all.

Upvotes: 7

Agust&#237; S&#225;nchez
Agust&#237; S&#225;nchez

Reputation: 11223

Don't you have a service layer? Preferences, parameters, default values and so on should be injected into service classes which are the ones centralizing business logic and they should be managed by Spring.

If you don't have a UserService, then load the default value into the controller.

I just notice the conversion from DTO to the domain class is taking place in the controller.

Define

@Value("${default.user.enabled:true}")  
private Boolean defaultUserEnabled;

inside the controller and then

if (user.isEnabled() == null)
    user.setEnabled(defaultUserEnabled);

But, as I already said, both the declaration and the setting of the default value belong to a Spring-managed service class.

Upvotes: 1

Related Questions