I created an annotation for creating ThreadPoolTaskExecutors populated with values from the environment. However, when I autowire the bean it gives me a proxy and calling the methods on the proxy gives the wrong values.
If I manually access the target class, then I get the correct values.
Executor exec = (Executor) ((Advised) executor).getTargetSource().getTarget();
ThreadPoolTaskExecutor taskExec = (ThreadPoolTaskExecutor) exec;
I have been scratching my head for a while now as to why I'm getting a proxy bean, but can't seem to figure it out.
I am using an annotation to import my registrar class that implements ImportBeanDefinitionRegistrar to register the bean. The registrar code is below:
public class ExecutorEnumerationRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
public static final String CORE_POOL_SIZE = "corePoolSize";
public static final String MAX_POOL_SIZE = "maxPoolSize";
public static final String QUEUE_CAPACITY = "queueCapacity";
public static final String THREAD_NAME_PREFIX = "threadNamePrefix";
private static final String REJECTED_EXECUTION_HANDLER = "rejectedExecutionHandler";
private static final String NAMES = "names";
private static final String REJECTED_HANDLER = "rejectedHandler";
private Environment env;
#Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> attrs = importingClassMetadata.getAnnotationAttributes(ThreadPoolTaskExecutorCreator.class.getName(), true);
final String[] beanNames = (String[]) attrs.get(NAMES);
final String[] policyClass = (String[]) attrs.get(REJECTED_HANDLER);
for (int x = 0; x < beanNames.length; x++) {
createAndRegisterBean(beanNames[x], policyClass[x], registry);
}
}
private void createAndRegisterBean(String name, String policyClass, BeanDefinitionRegistry registry) {
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setBeanClass(ThreadPoolTaskExecutor.class);
bd.setAutowireCandidate(true);
bd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
MutablePropertyValues mpv = bd.getPropertyValues();
populateProperties(mpv, name, policyClass);
registry.registerBeanDefinition(name, bd);
}
private void populateProperties(MutablePropertyValues mpv, String name, String policyClass) {
mpv.add(CORE_POOL_SIZE, Integer.valueOf(env.getProperty(name + "." + CORE_POOL_SIZE)));
mpv.add(MAX_POOL_SIZE, Integer.valueOf(env.getProperty(name + "." + MAX_POOL_SIZE)));
mpv.add(QUEUE_CAPACITY, Integer.valueOf(env.getProperty(name + "." + QUEUE_CAPACITY)));
try {
mpv.add(REJECTED_EXECUTION_HANDLER, Class.forName(policyClass).newInstance());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
mpv.add(THREAD_NAME_PREFIX, name + "-");
}
#Override
public void setEnvironment(Environment environment) {
env = environment;
}
}
Annotation to import the registrar:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Documented
#Import(ExecutorEnumerationRegistrar.class)
public #interface ThreadPoolTaskExecutorCreator{
String[] names();
String[] rejectedHandler() default ThreadPoolPolicyHandlers.CALLER_RUNS_POLICY;
}
I have tested with the following code:
Spring Boot Class:
#EnableDiscoveryClient
#ComponentScan("my.test.classes")
#ThreadPoolTaskExecutorCreator(names = {"testExecutor"}, rejectedHandler = ThreadPoolPolicyHandlers.DISCARD_POLICY)
#SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
SessionAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
JpaRepositoriesAutoConfiguration.class,
JndiDataSourceAutoConfiguration.class,
JndiConnectionFactoryAutoConfiguration.class,
RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class})
public class TestBoot {
public static void main(String[] args) {
SpringApplication.run(TestBoot.class, args);
}
}
All versions from spring-boot-starter-parent 1.4.5.RELEASE
I wrote a JUnit test that checks the values and it passes. The only time it doesn't work is when I autowire it in a Spring Boot eureka application. Is there anything I can do so that it doesn't autowire a proxy bean? I have searched through the documentation and looked at all the related classes, but I don't see anything related to why it's a proxy. Also, why does it give incorrect values when accessed through the proxy?
Seems you are missing the code for registering the instance of your ImportBeanDefinitionRegistrar ( in your example that is ExecutorEnumerationRegistrar )
So there are two ways to register the ImportBeanDefinitionRegistrar use the #Import annotation directly or implement the ImportSelector interface which can give you more generic configuration options.
For your case simply adding the #Import({ExecutorEnumerationRegistrar.class}) on the Configuration class will do the trick.
#EnableDiscoveryClient
#ComponentScan("my.test.classes")
#ThreadPoolTaskExecutorCreator(names = {"testExecutor"}, rejectedHandler = ThreadPoolPolicyHandlers.DISCARD_POLICY)
// THIS IS REQUIRED
#Import({ExecutorEnumerationRegistrar.class})
// See Above
#SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
SessionAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
JpaRepositoriesAutoConfiguration.class,
JndiDataSourceAutoConfiguration.class,
JndiConnectionFactoryAutoConfiguration.class,
RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class})
public class TestBoot {
public static void main(String[] args) {
SpringApplication.run(TestBoot.class, args);
}
}
and just remember to use the #Qualifier when autowiring the instance of ThreadPoolTaskExecutor. See example component
#Component
public class Component {
#Autowired
#Qualifier("testExecutor")
private ThreadPoolTaskExecutor exec;
// you methods
}
Related
i have a question here, please give some ideas.
I have two beans. FaceComparisonServerImpl depends on FaceServer.
When i want to test. I want to change the String in my 'FaceServer' bean.
#Service
public class FaceComparisonServerImpl implements FaceComparisonServer {
#Autowired
private FaceServer faceServer;
#Override
public FaceComparsionInfo getServerInfo() {
String serverInfo = faceServer.getServerInfo();
...
}
}
#Component
public class FaceServer {
#Autowired
private RestTemplate restTemplate;
//Not final, just to test.
private String version = "1.0";
private static final String CODE = "code";
private static final String MESSAGE = "message";
//Final
private static final String SERVER_URL = "http://127.0.0.1:8066/api/ZKComparison";
}
Bellow is my test code.
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = TestConfig.class)
public class FaceServerTestByTyler {
#Autowired
private FaceComparisonServer faceComparisonServer;
#Test
public void getServerInfo(){
//How can i modify the value of SERVER_URL in faceServer?
FaceComparsionInfo serverInfo = faceComparisonServer.getServerInfo();
System.out.println(serverInfo);
}
}
My question is:
How can i modified the value of 'version' and 'SERVER_URL' in #Bean(faceServer)?
Thanks you!
You need create FaceServer mock bean for test configuration.
And override required methods
#Configuration
Class TestConfig{
#Bean
#Primary
public FaceServer faceServer() {
return new FaceServer() {
#override
public String getServerInfo(){
return "required info";
}
};
}
}
The easiest way to customize the values is to make them Spring properties:
#Component
public class FaceServer {
#Value("${faceServer.version}")
private String version;
#Value("${faceServer.url}")
private String serverUrl;
// ...
}
You can either have default values for the #Value annotations or use some default property values in application.yml.
Now just override those properties in your test with the values you want:
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = TestConfig.class)
#TestPropertySource(properties = {
"faceServer.version=1.0",
"faceServer.url=http://127.0.0.1:8066/api/ZKComparison"
})
public class FaceServerTestByTyler {
#Autowired
private FaceComparisonServer faceComparisonServer;
// ...
}
However...
The second option is to make your classes more unit-testable. Prefer construction injection over field injection, and you can test your classes more independently.
#Service
public class FaceComparisonServerImpl implements FaceComparisonServer {
private final FaceServer faceServer;
public FaceComparisonServerImpl(FaceServer faceServer) {
this.faceServer = faceServer;
}
#Override
public FaceComparsionInfo getServerInfo() {
String serverInfo = faceServer.getServerInfo();
// ...
}
}
This now becomes unit-testable:
public class FaceServerTestByTyler {
private FaceComparisonServer faceComparisonServer;
private FaceServer faceServer;
#BeforeEach
public setup() {
faceServer = mock(FaceServer.class);
faceComparisonServer = new FaceComparisonServer(faceServer);
}
#Test
public void getServerInfo() {
when(faceServer.getServerInfo()).thenReturn(xxx);
// ...
}
}
The second option ends up with a test that runs much faster than any solutions that suggest to create a mock bean through a test configuration.
I'm using Spring boot - 2.3.3.RELEASE. There are some values in application.yaml which I'm trying to inject in the classes using #Value annotation. But for some reason, they are not loading up. The result should be that, in SendPhoneByPhoneNumbers.java, we should be able to read notificationServiceURL from application.yaml.
Note- I'm using Factory and Strategy pattern. This project is going to be used as a Library for other projects to import and use the methods exposed by Service layer.
Here is the folder structure: https://imgur.com/a/jYr7wyP
I'm trying to test by running Demo.java in Debug mode to see how the actual values looks like.
application.yaml
notificationService:
url: "https://someURL.com"
Demo.java
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
#SpringBootApplication
public class Demo {
public static void main(String[] args) {
SpringApplication.run(Demo.class, args);
String title="Title";
String message="message";
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("333-222-1111");
PhoneService phoneService = new PhoneService();
phoneService.sendNotificationByPhoneNumbers(title, message, phoneNumbers);
}
}
PhoneService.java
#Service
public class PhoneService {
PhoneServiceImpl notificationServiceImpl = new PhoneServiceImpl();
public void sendNotificationByPhoneNumbers(String title, String message, List<String> phoneNumbers) {
notificationServiceImpl.sendNotificationByPhoneNumbers(title, message, phoneNumbers);
}
}
PhoneServiceImpl.java
#Slf4j
#Component
public class PhoneServiceImpl {
#Value("${notificationService.url}")
String url;
public void sendNotificationByPhoneNumbers(String title, String message, List<String> phoneNumbers) {
PhoneContext phoneContext = new PhoneContext(new SendPhoneByPhoneNumbers(url));
phoneContext.notify(title, message, phoneNumbers);
}
}
PhoneContext.java
public class PhoneContext {
private PhoneStrategy phoneStrategy;
public PhoneContext(PhoneStrategy phoneStrategy){
this.phoneStrategy = phoneStrategy;
}
public void notify(String title, String message, List<String> employees){
phoneStrategy.sendNotification(title, message, employees);
}
}
PhoneStrategy.java
public interface PhoneStrategy {
public void sendNotification(String title, String message, List<String> listOfEmployeeIdGroupNamePhoneNumbers);
}
SendPhoneByPhoneNumbers.java
#Slf4j
public class SendPhoneByPhoneNumbers implements PhoneStrategy {
RestTemplate restTemplate;
String notificationServiceURL;
BuildHttpRequest buildHttpRequest;
public SendPhoneByPhoneNumbers(String notificationServiceURL) {
this.notificationServiceURL = notificationServiceURL;
this.restTemplate = new RestTemplate();
this.buildHttpRequest = new BuildHttpRequest();
}
#Async
public void sendNotification(String title, String message, List<String> phoneNumbers) {
SmsMessage smsMessage= new SmsMessage(title, message, phoneNumbers, Collections.emptyList(), Collections.emptyList());
try {
HttpHeaders headers = new HttpHeaders();
headers.set("idToken", buildHttpRequest.getNewToken());
HttpEntity<SmsMessage> newRequest = new HttpEntity<>(smsMessage, headers);
restTemplate.postForObject(notificationServiceURL + "/someUrl", newRequest, String.class);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
Also, if you guys have any suggestions on modifying the code/structure in an way to make it better, please do suggest.
Thanks in advance.
PhoneService phoneService = new PhoneService();
Since PhoneService that you are using is not a managed bean by spring container, the values are not being injected.
Code Improvement and FIX
String title="Title";
String message="message";
List<String> phoneNumbers = new ArrayList<>();
phoneNumbers.add("333-222-1111");
// PhoneService phoneService = new PhoneService();
phoneService.sendNotificationByPhoneNumbers(title, message, phoneNumbers);
Move this code to a class implementing CommanLineRunner or ApplicationRunner and override corresponding run(). In this class, you could #Autowire PhoneService rather than manually instantiating it. Also note that you have to mark this class with #Component
Other minor suggested changes:
You can make SendPhoneByPhoneNumbers class a singleton. If there are multiple implementions, use #Qualifier
Both RestTemplate and BuildHttpRequest could be created using #Bean annotation.
Since you are using lombok, using #RequiredArgsConstructor could also be considered.
I have an application that uses SpringBoot for dependency injection and the app works fine, but testing fails because #Autowired fields aren't being injected during tests.
#SpringBootApplication
public class ProcessorInterface {
protected final static Logger logger = Logger.getLogger( ProcessorInterface.class );
public static void main(String[] args) {
try {
SpringApplication.run(ProcessorInterfaceRunner.class, args);
} catch (Exception ex) {
logger.error("Error running ProcessorInterface", ex);
}
}
}
#Component
#Configuration
#ComponentScan
public class ProcessorInterfaceRunner implements CommandLineRunner {
protected final static Logger logger = Logger.getLogger( ProcessorInterface.class );
#Autowired
private RequestService requestService = null;
#Autowired
private ValidatorService validatorService = null;
#Override
public void run(String... args) throws Exception {
ESPOutTransaction outTransaction = null;
outTransaction = new ESPOutTransaction();
// initialize outTransaction fields
...
// done initializing outTransaction fields
if (validatorService.isValid(outTransaction)) {
System.out.println(requestService.getRequest(outTransaction));
} else {
System.out.println("Bad Data");
}
}
}
#Service
public class ESPRequestService implements RequestService<ESPOutTransaction> {
#Autowired
ValidatorService validatorService = null;
#Override
public String getRequest(ESPOutTransaction outTransaction) throws IllegalArgumentException {
if (!validatorService.isValid(outTransaction)) {
throw new IllegalArgumentException("Invalid parameters in transaction object. " + outTransaction.toString());
}
StringBuffer buff = new StringBuffer("create request XML");
buff.append("more XML");
return buff.toString();
}
}
#Service
public class ESPValidatorService implements ValidatorService {
private static org.apache.log4j.Logger logger = Logger.getLogger(ESPValidatorService.class);
// declare some constants for rules
private static final int MAX_LENGTH_XYZ = 3;
#Override
public boolean isValid(OutTransaction outTransaction) {
ESPOutTransaction espOutTransaction = (ESPOutTransaction)outTransaction;
boolean isValid = true;
if (espOutTransaction == null) {
logger.warn("espOutTransaction is NULL");
isValid = false;
} else {
// XYZ is required
if (espOutTransaction.getXYZ() == null) {
logger.warn("XYZis NULL\r\n" + espOutTransaction.toString());
isValid = false;
}
// XYZ max length = MAX_LENGTH_XYZ
if (espOutTransaction.getXYZ() != null && espOutTransaction.getPubCode().trim().length() > MAX_LENGTH_XYZ) {
logger.warn("XYZis too long (max length " + MAX_LENGTH_XYZ + ")\r\n" + espOutTransaction.toString());
isValid = false;
}
}
return isValid;
}
}
These all work and I get good output when I run the app. When I try to test it though, it fails because it can't find ESPValidatorService to inject into ESPRequestService
#RunWith(Suite.class)
#SuiteClasses({ ESPOutTransactionValidatorTest.class, ESPRequestTest.class })
public class AllTests {}
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {ESPRequestService.class})
public class ESPRequestTest {
#Test
public void testGetRequest() {
ESPRequestService requestService = new ESPRequestService();
String XYZ = "XYZ";
ESPOutTransaction outTransaction = null;
outTransaction = new ESPOutTransaction();
outTransaction.setXYZ(XYZ);
String strRequest = "some expected request XML";
String request = requestService.getRequest(outTransaction);
assertEquals(request, strRequest);
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(classes = ESPValidatorService.class)
public class ESPOutTransactionValidatorTest {
#Test
public void testIsValid() {
ESPValidatorService validatorService = new ESPValidatorService();
ESPOutTransaction outTransaction = null;
// test request = null
assertFalse(validatorService.isValid(outTransaction));
String XYZ = "XYZ";
outTransaction = new ESPOutTransaction();
outTransaction.setXYZ(XYZ);
// test all good
assertTrue(validatorService.isValid(outTransaction));
// test XYZ
outTransaction.setXYZ(null);
assertFalse(validatorService.isValid(outTransaction));
outTransaction.setXYZ("ABCD"); // too long
assertFalse(validatorService.isValid(outTransaction));
outTransaction.setXYZ(XYZ);
}
}
How can I get the unit tests to auto wire?
I see two problems :
1) you don't rely on Spring beans but you create instances with the new operator.
Instead of writing :
ESPRequestService requestService = new ESPRequestService();
you should let Spring inject the instance :
#Bean
ESPRequestService requestService;
2) The #SpringBootTest configuration is not correct.
In each test, you specified a very specific bean class in the classes attribute of #SpringBootTest :
#SpringBootTest(classes = ESPValidatorService.class)
public class ESPOutTransactionValidatorTest {
and
#SpringBootTest(classes = {ESPRequestService.class})
public class ESPRequestTest {
But classes attributes of #SpringBootTest serves to specify the annotated classes to use for loading an ApplicationContext.
The annotated classes to use for loading an ApplicationContext. Can
also be specified using #ContextConfiguration(classes=...). If no
explicit classes are defined the test will look for nested
#Configuration classes, before falling back to a
SpringBootConfiguration search.
So all configuration classes and beans of your application may not be discovered and loaded in the Spring container .
To be able to load all application beans during your tests, the most simple way is not specifying the classes attribute in the #SpringBootTest annotation :
#SpringBootTest
public class ESPRequestTest { ...}
It will look for a Spring bean that holds the #SpringBootConfiguration.
Ideally, it will found the #SpringBootApplication bean of your application.
If the package of the test class is located inside the package (or at a lower level) of the #SpringBootApplication class, it should be automatically discovered.
Otherwise the other way is specifying a configuration that will allow to load all required beans :
#SpringBootTest(classes = MySpringBootApplication.class)
public class ESPRequestTest { ...}
I've created the MCVE below. The service should get a message and replace the vowels with a predefined ugly.char coming from application.properties.
application.properties:
ugly.char=x
UglifyService.java:
public interface UglifyService {
String uglifyMessage(String message);
}
UglifyServiceImpl.java:
#Service
public class UglifyServiceImpl implements UglifyService {
#Value("${ugly.char}")
private char uglyCharFromAppProp;
private final char uglyChar;
#Autowired
public UglifyServiceImpl() {
this.uglyChar = uglyCharFromAppProp;
}
#Override
public String uglifyMessage(String message) {
return message.replaceAll("[aeiouAEIOU]", String.valueOf(uglyChar));
}
}
UglyCharController.java:
#Controller
public class UglyCharController {
private final UglifyService uglifyService;
#Autowired
public UglyCharController(UglifyService uglifyService) {
this.uglifyService = uglifyService;
}
#Value("${ugly.char}")
private char uglyChar;
#RequestMapping("/")
#ResponseBody
public String index() {
return "Usage: http://localhost:8080/some-message";
}
#GetMapping("/{message:.+}")
#ResponseBody
public String uglifyMessage(#PathVariable String message) {
String uglyMessage = uglifyService.uglifyMessage(message);
return "The ugly char is: '"+ uglyChar +"'." +
"<br>The uglifyed message is: "+ uglyMessage;
}
}
Application.java:
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The problem is that, instead of replacing the vowels, the service is just removing them.
When requesting localhost:8080/mymessage the response should be mymxssxgx, but I am getting mymssg.
My questions are:
Why the service is not working as expected?
What would be the right way to make it work, considering that I would like the uglyChar to be final inside the singleton service and to come from application.properties?
Note: The service starts to work if I replace String.valueOf(uglyChar) with String.valueOf(uglyCharFromAppProp), but uglyChar would not be final, as I would like it to be.
When object is instantiated, first the constructor is executed and then the values of the properties are set and #Value is executed. For this reason the value of uglyChar does not have the value of the property.
You can do it with:
private final char uglyChar;
#Autowired
public UglifyServiceImpl(#Value("${ugly.char}") final char uglyCharFromAppProp){
this.uglyChar = uglyCharFromAppProp;
}
I'm developing a web application with spring. I've had no problem autowiring and using database #Service classes. Now I'm trying to read a global property file and provide the values to all classes that need them. The solution I've come up with so far seem to be overly complicated (too many classes - AppConfig, ServerConfig iface, ElasticServerConfig) for such a trivial task but I could live with it if it worked.
my applicationContext.xml contains
<context:component-scan base-package="my.package" />
AppConfig.java:
package my.package.configuration;
#Configuration
#PropertySource("classpath:application.properties")
public class AppConfig {
}
ServerConfig.java:
public interface ServerConfig {
String getUrl();
String getUser();
String getPassword();
}
ElasticSearchConfig.java:
package my.package.configuration;
#Component(value = "elasticServerConfig")
public class ElasticServerConfig implements ServerConfig {
private static final Logger LOGGER = LogManager.getLogger(ElasticServerConfig.class);
private String url;
private String user;
private String password;
#Autowired
public ElasticServerConfig(final Environment env) {
this.url = env.getProperty("elastic_server.url");
this.user = env.getProperty("elastic_server.user");
this.password = env.getProperty("elastic_server.password");
LOGGER.debug("url=" + url + "; user=" + user + "; password=" + password); // this works!
}
#Override
public final String getUrl() {
return url;
}
#Override
public final String getUser() {
return user;
}
#Override
public final String getPassword() {
return password;
}
}
When the web application boots, the ElasticServerConfig constructor prints out the correct url/user/pwd as read from application.properties. However an instance of ElasticServerConfig is not injected into a Search object:
package my.package.util;
public class Search {
#Autowired
#Qualifier("elasticServerConfig")
private ServerConfig elasticServerConfig;
public final List<Foobar> findByPatternAndLocation() {
if (elasticServerConfig == null) {
LOGGER.error("elasticServerConfig is null!");
}
// and i get a NullPointerException further on
// snip
}
}
You have to register the Search class as a Spring Bean and take it from the Spring context when you want to use it. It's important to get the bean from the spring context. If you create an object of that class with new, Spring has no way to know about that class and mange it's dependencies.
You can get get a bean from the Spring context by #Autowire it somewhere or by accessing an instance of the context and use the getBean method:
#Configuration
#PropertySource("classpath:application.properties")
public class AppConfig {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(AppConfig.class, args);
ctx.getBean...
}
}
Either use #Component annotation on the class and make sure that the class is in package thats under my.package
or register it in the configuration class
#Configuration
#PropertySource("classpath:application.properties")
public class AppConfig {
#Bean
public Search search(){
return new Search();
}
}