Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
    }

}
like image 646
jscherman Avatar asked Aug 10 '15 18:08

jscherman


1 Answers

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.

like image 155
Constantine Avatar answered Sep 19 '22 14:09

Constantine