I'm using spring data + hbase to write some values into a HBase database.
Unfortunately the HbaseTemplate seems to close the connection after the first call.
I'm new to Spring and HBase/Hadoop, so i don't know if this is a Spring/HBase Configuration issue or another stupidity
Testclass:
package org.springframework.data.hadoop.samples;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.hadoop.hbase.HbaseTemplate;
import org.springframework.data.hadoop.hbase.TableCallback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration("/hbase-context.xml")
public class WordCountWorkflowTest {
#Autowired
private ApplicationContext ctx;
#Autowired
private HbaseTemplate hbaseTemplate;
#Test
public void testWorkflowNS() throws Exception {
if (hbaseTemplate == null) {
throw new NullPointerException("template null!");
}
// Write to HBase
InnerTableCallback itc = new InnerTableCallback("JustaString", 42);
hbaseTemplate.execute("Wordcount", itc);
itc = new InnerTableCallback("Anotherstring", 23);
// Here the HBase insert fails
hbaseTemplate.execute("Wordcount", itc);
}
#Test
public void testWorkflowNSSucess() throws Exception {
System.out.println("done");
}
/**
* This is a Inner class providing access to the HBase Table to store the
* counted words and number of matches.
*
* */
class InnerTableCallback implements TableCallback<Object> {
String foundStr;
int no;
/**
* The constructor just saved the given foundStr/no tuple in inner
* variables.
*
* #param foundstr
* string found in the text
* #param no
* number of found matches
* #return null
* */
public InnerTableCallback(String foundStr, int no) {
this.foundStr = foundStr;
this.no = no;
}
/**
* This Method puts the given String and number of found matches into
* the HBase table the column family is "cf1" and the column is
* "matches". The rowname is the found string.
* */
#Override
public Object doInTable(HTable table) throws Throwable {
Put p = new Put(Bytes.toBytes(foundStr));
// Put operation on hbase shell:
// hbase(main):005:0> put 'testtable', 'myrow-2', 'colfam1:q2',
// 'value-2'
// add(byte[] family, byte[] qualifier, byte[] value)
p.add(Bytes.toBytes("cf1"), Bytes.toBytes("matches"),
Bytes.toBytes(new Integer(no).toString()));
table.put(p);
return null;
}
}
}
hbase-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:hdp="http://www.springframework.org/schema/hadoop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/hadoop http://www.springframework.org/schema/hadoop/spring-hadoop.xsd">
<context:property-placeholder location="classpath:batch.properties,classpath:hadoop.properties"
ignore-resource-not-found="true" ignore-unresolvable="true" />
<context:component-scan base-package="org.springframework.data.hadoop.samples" />
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
<bean id="hbaseTemplate" class="org.springframework.data.hadoop.hbase.HbaseTemplate" p:configuration-ref="hbaseConfiguration"/>
<hdp:hbase-configuration>
</hdp:hbase-configuration>
</beans>
Stacktrace:
org.springframework.data.hadoop.hbase.HbaseSystemException: org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation#61bb0cc0 closed; nested exception is java.io.IOException: org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation#61bb0cc0 closed
at org.springframework.data.hadoop.hbase.HbaseUtils.convertHbaseException(HbaseUtils.java:42)
at org.springframework.data.hadoop.hbase.HbaseTemplate.convertHbaseAccessException(HbaseTemplate.java:111)
at org.springframework.data.hadoop.hbase.HbaseTemplate.execute(HbaseTemplate.java:82)
at org.springframework.data.hadoop.samples.WordCountWorkflowTest.testWorkflowNS(WordCountWorkflowTest.java:35)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
....
Caused by: java.io.IOException: org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation#61bb0cc0 closed
at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.locateRegion(HConnectionManager.java:822)
at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.locateRegion(HConnectionManager.java:810)
at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.processBatchCallback(HConnectionManager.java:1492)
at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.processBatch(HConnectionManager.java:1377)
at org.apache.hadoop.hbase.client.HTable.flushCommits(HTable.java:916)
at org.apache.hadoop.hbase.client.HTable.doPut(HTable.java:772)
at org.apache.hadoop.hbase.client.HTable.put(HTable.java:747)
at org.springframework.data.hadoop.samples.WordCountWorkflowTest$InnerTableCallback.doInTable(WordCountWorkflowTest.java:83)
at org.springframework.data.hadoop.hbase.HbaseTemplate.execute(HbaseTemplate.java:72)
Cheers,R
Solved!
I've used a older and deprecated version of the spring-data-hadoop package (Milestone) in my maven pom.xml. I switched to the Snapshot repository, which fixes the incorrect handling of HBase tables.
If you use spring-batch: I had to change the <tasklet> definitions for hadoop to <job-tasklet> in the *context.xml with the new version.
The error message from the XML parser was:
The matching wildcard is strict, but no declaration can be found for element 'tasklet'
Hope this helps someone :-)
Related
I am Running Spring Core program and I got this Error. and I configure build path and add all jar files.
structure of project
WishMessageGenerator.java
package com.nt.beans;
import java.util.Date;
public class WishMessageGenerator {
private Date date;
public WishMessageGenerator() {
System.out.println("WishMessageGenerator:0-param constructor");
}
public void setDate(Date date) {
System.out.println("WishMessageGenerator:setDate(-)");
this.date = date;
}
public String generateMessage(String user) {
int hour = 0;
System.out.println("Injected date::"+date);
hour = date.getHours();
if(hour < 12)
return "Good Morning :::"+user;
else if(hour < 16)
return "Good AfterNoon :::"+user;
else if(hour < 20)
return "Good Evening :::"+user;
else
return "Good Night"+user;
}
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Dependent Bean class cfg -->
<bean id="dt" class="java.util.Date" />
<!-- Target Bean class cfg -->
<bean id="wmg" class="com.nt.beans.WishMessageGenerator">
<property name="date" ref="dt"></property><!-- Setter injection -->
</bean>
</beans>
SetterInection.java
package com.nt.test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import com.nt.beans.WishMessageGenerator;
public class SetterInjection {
public static void main(String[] args) {
Resource res = null;
BeanFactory factory = null;
Object obj = null;
WishMessageGenerator generator = null;
res = new FileSystemResource("src/com/nt/cfgs/applicationContext.xml");
factory = new XmlBeanFactory(res);
obj = factory.getBean("wmg");
generator = (WishMessageGenerator)obj;
System.out.println("result:::"+generator.generateMessage("KingRk"));
}
}
I got this output:
Error: Unable to initialize main class com.nt.test.SetterInjection
Caused by: java.lang.NoClassDefFoundError: org/springframework/core/io/Resource
The output is:
we have to add jar into class path not module path
Add jar into class path using Build path
Buid path
I read the documentation here, but I think that the Spring documentation is sometimes complex and hard to understand, so I need a little explanation about BeanNameViewResolver.
Can I get a code example?
As explained in the documentation,BeanNameViewResolver resolves Views declared as beans. Most of the time you need it for some special-purpose views.
For example, we need to render an Excel spreadsheet.
For the Excel generation you will Apache POI
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.6</version>
</dependency>
So, you subclass AbstractExcelView and implement your custom logic to render a spreadsheet based on model values.
public class CustomExcelView extends AbstractExcelView {
#Override
protected void buildExcelDocument(Map model, HSSFWorkbook workbook,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
Map<String,String> revenueData = (Map<String,String>) model.get("revenueData");
//create a wordsheet
HSSFSheet sheet = workbook.createSheet("Revenue Report");
HSSFRow header = sheet.createRow(0);
header.createCell(0).setCellValue("Month");
header.createCell(1).setCellValue("Revenue");
int rowNum = 1;
for (Map.Entry<String, String> entry : revenueData.entrySet()) {
//create the row data
HSSFRow row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(entry.getKey());
row.createCell(1).setCellValue(entry.getValue());
}
}
}
and the controller simply adds the data
package com.example;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
#RequestMapping("/excelview")
public class ExcelController {
public String getExcel(Model model){
Map<String,String> revenueData = new HashMap<String,String>();
revenueData.put("Jan-2010", "$100,000,000");
revenueData.put("Feb-2010", "$110,000,000");
revenueData.put("Mar-2010", "$130,000,000");
revenueData.put("Apr-2010", "$140,000,000");
revenueData.put("May-2010", "$200,000,000");
model.addAttribute("revenueData",revenueData);
return "myExcelView";
}
}
Then declaring an BeanNameViewResolver makes it available to controllers: when controller returns String with view name myExcelView, your spreadsheet will be rendered.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.example" />
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
<bean id="myExcelView" class="com.example.CustomExcelView" />
</beans>
In an attempt to install my component on karaf I get the following error:
Caused by: org.apache.camel.CamelException: Cannot find any routes with this RouteBuilder reference: RouteBuilderRef[logparserRouteBean]
I've narrowed it down to a conversion error in AbstractBeanFactory using the SimpleTypeConverter as returned by the getTypeConverter().
Given that PerformanceLogRoute extends org.apache.camel.builder.RouteBuilder, how can the convertion fail??
Suggestions, and ideas to any solution is greatly appreciated.
UPDATE
package no.osl.cdms.profile.routes;
import no.osl.cdms.profile.api.TimeMeasurement;
import no.osl.cdms.profile.factories.EntityFactory;
import no.osl.cdms.profile.log.TimeMeasurementEntity;
import no.osl.cdms.profile.parser.LogLineRegexParser;
import org.apache.camel.builder.RouteBuilder;
import java.util.Map;
public class PerformanceLogRoute extends RouteBuilder {
public static final String PERFORMANCE_LOG_ROUTE_ID = "PerformanceLogRoute";
private static final String LOG_DIRECTORY = "C:/data";
private static final String LOG_FILE = "performance.log";
private static final int DELAY = 0;
private LogLineRegexParser logLineRegexParser = new LogLineRegexParser();
private EntityFactory entityFactory = EntityFactory.getInstance();
private static final String LOG_FILE_ENDPOINT = "stream:file? fileName="+LOG_DIRECTORY +"/"+LOG_FILE+"&scanStream=true&scanStreamDelay=" + DELAY;
private static final String DATABASE_ENDPOINT = "jpa:";
#Override
public void configure() throws Exception{
fromF(LOG_FILE_ENDPOINT, LOG_DIRECTORY, LOG_FILE, DELAY)
.convertBodyTo(String.class) // Converts input to String
.choice().when(body().isGreaterThan("")) // Ignores empty lines
.bean(logLineRegexParser, "parse") // Parses log entry into String map
.bean(entityFactory, "createTimemeasurement") // Parses log entry into database format
.split(body())
.choice().when(body().isNotNull())
.toF(DATABASE_ENDPOINT, body().getClass().toString())
.routeId(PERFORMANCE_LOG_ROUTE_ID);
}
public String toString() {
return PERFORMANCE_LOG_ROUTE_ID;
}
}
The xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:camel="http://camel.apache.org/schema/spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd">
<bean id="logparserRouteBean" class="no.osl.cdms.profile.routes.PerformanceLogRoute" />
<camelContext id="cdms-core-camel-context" xmlns="http://camel.apache.org/schema/spring">
<routeBuilder ref="logparserRouteBean" />
</camelContext>
</beans>
This is what I found at this moment. From what I remember it is identical to what caused the error, but I'll double check in the morning.
I am trying to post a message to my twitter account using Spring Integration with Twitter with a standalone program on my windows XP machine. But I am getting the following error -
WARNING: POST request for "https://api.twitter.com/1/statuses/update.json" resulted in 401 (Unauthorized); invoking error handler
Exception in thread "main" org.springframework.integration.MessageHandlingException: error occurred in message handler [org.springframework.integration.twitter.outbound.StatusUpdatingMessageHandler#0]
at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:79)
at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:115)
at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:102)
at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:77)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:157)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:128)
at com.skilledmonster.spring.integration.twitter.TwitterOutbound.main(TwitterOutbound.java:20)
Caused by: org.springframework.social.RevokedAuthorizationException: The authorization has been revoked. Reason: Unknown
at org.springframework.social.twitter.api.impl.TwitterErrorHandler.handleClientErrors(TwitterErrorHandler.java:96)
at org.springframework.social.twitter.api.impl.TwitterErrorHandler.handleError(TwitterErrorHandler.java:58)
at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:486)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:443)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:415)
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:294)
at org.springframework.social.twitter.api.impl.TimelineTemplate.updateStatus(TimelineTemplate.java:236)
at org.springframework.social.twitter.api.impl.TimelineTemplate.updateStatus(TimelineTemplate.java:224)
at org.springframework.integration.twitter.outbound.StatusUpdatingMessageHandler.handleMessageInternal(StatusUpdatingMessageHandler.java:57)
at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:73)
... 6 more
Here is my code -
twitter-outbound.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
http://www.springframework.org/schema/integration/twitter/spring-integration-twitter-2.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration-2.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan
base-package="com.apress.prospringintegration.social.twitter" />
<context:property-placeholder location="/twitter.properties" />
<int:channel id="twitterOutbound" />
<twitter:outbound-channel-adapter twitter-template="twitterTemplate" channel="twitterOutbound" />
<bean id="twitterTemplate"
class="org.springframework.social.twitter.api.impl.TwitterTemplate">
<constructor-arg value="${twitter.consumer-key}" />
<constructor-arg value="${twitter.consumer-secret}" />
<constructor-arg value="${twitter.access-token}" />
<constructor-arg value="${twitter.access-token-secret}" />
</bean>
</beans>
TwitterConfigurationTemplate.java
package com.skilledmonster.spring.integration.twitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.twitter.api.impl.TwitterTemplate;
#Configuration
public class TwitterConfigurationTemplate {
#Value("${consumer-key}")
private String consumerKey;
#Value("${consumer-secret}")
private String consumerSecret;
#Value("${access-token}")
private String accessToken;
#Value("${access-token-secret}")
private String accessTokenSecret;
#Bean
public TwitterTemplate twitterTemplate() {
TwitterTemplate twitterOperations =
new TwitterTemplate(
consumerKey, consumerSecret, accessToken, accessTokenSecret);
return twitterOperations;
}
}
TwitterOutbound.java
package com.skilledmonster.spring.integration.twitter;
import java.util.Calendar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.message.GenericMessage;
public class TwitterOutbound {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("/twitter-outbound.xml", TwitterOutbound.class);
MessageChannel input = context.getBean("twitterOutbound", MessageChannel.class);
Message<String> message = new GenericMessage<String>("Testing new Twitter samples for #springintegration"+Calendar.getInstance().getTimeInMillis());
input.send(message);
}
}
twitter.properties
twitter.consumer-key=onj5cG1P9pe7n9qA8UI4EA
twitter.consumer-secret=2l7hqMafKYaTkVBW3YfuBfGdzCtmICOJwjOOCEeQ
twitter.access-token=792995125-dXmN1Pbw7sE4WttvAbX7ssxEn4lHaVd6uOX3IMxk
twitter.access-token-secret=a1EuvvONcphdqXpfJVjCdaIBDlMZSUL5pgimWuEtg
FYI - I did test my twitter token and key with twitter4j and it seems to be posting the message to twitter successfully.
Here is my code with twitter4j -
OAuthSetup.java
package com.skilledmonster.spring.integration.twitter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Calendar;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
public class OAuthSetup {
/**
* #param args
*/
public static void main(String args[]) throws Exception {
// The factory instance is re-useable and thread safe.
Twitter twitter = new TwitterFactory().getInstance();
//insert the appropriate consumer key and consumer secret here
twitter.setOAuthConsumer("onj5cG1P9pe7n9qA8UI4EA",
"2l7hqMafKYaTkVBW3YfuBfGdzCtmICOJwjOOCEeQ");
RequestToken requestToken = twitter.getOAuthRequestToken();
AccessToken accessToken = null;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (null == accessToken) {
System.out.println("Open the following URL and grant access to your account:");
System.out.println(requestToken.getAuthorizationURL());
System.out.print("Enter the PIN(if aviailable) or just hit enter.[PIN]:");
String pin = br.readLine();
try{
if(pin.length() > 0){
accessToken = twitter.getOAuthAccessToken(requestToken, pin);
}else{
accessToken = twitter.getOAuthAccessToken();
}
} catch (TwitterException te) {
if(401 == te.getStatusCode()){
System.out.println("Unable to get the access token.");
}else{
te.printStackTrace();
}
}
}
//persist to the accessToken for future reference.
System.out.println(twitter.verifyCredentials().getId());
System.out.println("token : " + accessToken.getToken());
System.out.println("tokenSecret : " + accessToken.getTokenSecret());
//storeAccessToken(twitter.verifyCredentials().getId() , accessToken);
Status status = twitter.updateStatus("Testing new Twitter samples for springintegration # "+Calendar.getInstance().getTimeInMillis());
System.out.println("Successfully updated the status to [" + status.getText() + "].");
System.exit(0);
}
}
Can anyone suggest me what is going wrong here?
The token you are using to access Twitter has been revoked, or possibly not valid for the consumer id / application id you are using.
org.springframework.social.RevokedAuthorizationException: The authorization has been revoked
You will need to perform a new authorisation to obtain a token and secret for use with your application, or alternatively, on the twitter application configuration page (https://dev.twitter.com/apps/) you can generate a token specifically for your personal usage. This is useful for testing an app conveniently without having to perform OAuth.
I figured the issue was with the empty spaces in the key values in the properties file :-)
Here is a full example on my blog.
Does anyone know of a Maven plugin that can be used to validate Spring configuration files? By validation, I mean:
Verify all beans reference a class on the build path
Verify all bean references refer to a valid bean definition
Verify no orphaned beans exist
Other configuration mistakes I'm sure I'm missing.
I searched around and didn't come up with anything.
A Maven plugin would be ideal for my purposes, but any other tools (Eclipse plugin, etc.) would be appreciated.
What we do on our project is simply write a JUnit test which loads the Spring configuration. This does a few of the things you described like:
Validate the XML
Ensures beans can be loaded with classes on the classpath (at least beans which aren't lazy-loaded)
It does not check that there are no orphan beans. There is no reliable way of doing this anyway considering from anywhere in your code, you can lookup beans directly given their ID. Just because a bean is not referenced by any other beans does not mean it is not used. In fact all Spring configs will have at least one bean which is not referenced by other beans because there always has to be a root to the hierarchy.
If you have beans which rely on real services like databases or something and you don't want to connect to these services in a JUnit test, you simply need to abstract the configuration to allow for test values. This can be easily accomplished with something like the PropertyPlaceholderConfigurer which allows you to have different properties specified in separate config files for each environment and then referenced by one beans definition file.
EDIT (to include sample code):
The way we do this is have at least 3 different spring files...
src/main/resources/applicationContext.xml
src/main/resources/beanDefinitions.xml
src/test/resources/testContext.xml
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:beanDefinitions.xml"/>
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="file:path/environment.properties" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${driver}" />
...
</bean>
... <!-- more beans which shouldn't be loaded in a test go here -->
</beans>
beanDefinitions.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="myBean" class="com.example.MyClass">
...
</bean>
<bean id="myRepo" class="com.example.MyRepository">
<property name="dataSource" ref="dataSource"/>
...
</bean>
... <!-- more beans which should be loaded in a test -->
</beans>
testContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:beanDefinitions.xml"/>
<bean id="dataSource" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.jdbc.datasource.DriverManagerDataSource"/>
</bean>
</beans>
There are many things going on here, let me explain...
The applicationContext.xml file is the main spring file for your whole application. It contains an PropertyPlaceHolder bean to allow certain property values to be configurable between different environments we deploy to (test vs. prod). It imports all of the main beans that the app needs to run. Any beans which should not be used in a test, like DB beans, or other classes which communicate with external services/resources should be definied in this file.
The beanDefinitions.xml file has all of your normal beans in it which don't rely on external things. These beans can and will reference beans defined in the appContext.xml file.
The testContext.xml file is the test version of the appContext. It needs versions of all beans defined in the appContext.xml file but we used a mocking library to instantiate these beans. This way the real classes aren't used and there is no risk of access external resources. This file also doesn't need the property placeholder bean.
Now that we have a test context which we aren't afraid to load from a test, here is the code to do it...
SpringContextTest.java
package com.example;
import org.junit.Test;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
public class SpringContextTest {
#Test
public void springContextCanLoad() {
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("testContext.xml"));
for (String beanName : factory.getBeanDefinitionNames()) {
Object bean = factory.getBean(beanName);
// assert anything you want
}
}
}
This may not be the optimal way of doing it; the ApplicationContext class is the recommended way of loading spring contexts. The above might be able to be replaced by:
#Test
public void springContextCanLoad() {
ApplicationContext context = new FileSystemXmlApplicationContext("classpath:testContext.xml");
}
I believe that one line will accomplish everything you need to verify your spring context is wired correctly. From there, you can load beans and assert like before.
Hope this helps!
Here's the URL of Spring IDE update site (Eclipse plugin). It does what you described above. Their site seems to be unavailable.
I came across this question when googling - I had exactly the same question.
I've written a (very much untested) Maven plugin to do this this. It currently only supports WARs but could easily be extended. In addition, I don't bother actually loading the beans since I don't want the hassle of having to maintain a large set of properties just to satisfy this plugin.
Here it is if it's ever any use:
package myplugins;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.ClassUtils;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Validates Spring configuration resource and class references
* using a classloader that looks at the specified WAR's lib and classes
* directory.
* <p/>
* It doesn't attempt to load the application context as to avoid the
* need to supply property files
* <br/>
* TODO: maybe one day supplying properties will become an optional part of the validation.
*
* #goal validate
* #aggregator
* #phase install
*/
public class WarSpringValidationMojo extends AbstractMojo
{
private final static String FILE_SEPARATOR = System.getProperty("file.separator");
/**
* Project.
* #parameter expression="${project}"
* #readonly
*/
private MavenProject project;
/**
* The WAR's root Spring configuration file name.
*
* #parameter expression="${applicationContext}" default-value="webAppConfig.xml"
*/
private String applicationContext;
/**
* The WAR's directory.
*
* #parameter expression="${warSourceDirectory}" default-value="${basedir}/target/${project.build.finalName}"
*/
private File warSourceDirectory;
#SuppressWarnings("unchecked")
public void execute() throws MojoExecutionException
{
try
{
if ("war".equals(project.getArtifact().getType()))
{
File applicationContextFile = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + applicationContext);
File classesDir = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + "classes");
File libDir = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + "lib");
Set<URL> classUrls = new HashSet<URL>();
if (classesDir.exists())
{
classUrls.addAll(getUrlsForExtension(classesDir, "class", "properties"));
}
if (libDir.exists())
{
classUrls.addAll(getUrlsForExtension(libDir, "jar", "zip"));
}
ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader = new URLClassLoader(classUrls.toArray(new URL[classUrls.size()]), parentClassLoader);
ClassUtils.overrideThreadContextClassLoader(classLoader);
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.setBeanClassLoader(classLoader);
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.setValidating(true);
reader.loadBeanDefinitions(new FileSystemResource(applicationContextFile));
for (String beanName : factory.getBeanDefinitionNames())
{
validateBeanDefinition(classLoader, factory.getBeanDefinition(beanName), beanName);
}
getLog().info("Successfully validated Spring configuration (NOTE: validation only checks classes, " +
"property setter methods and resource references)");
}
else
{
getLog().info("Skipping validation since project artifact is not a WAR");
}
}
catch (Exception e)
{
getLog().error("Loading Spring beans threw an exception", e);
throw new MojoExecutionException("Failed to validate Spring configuration");
}
}
private void validateBeanDefinition(ClassLoader beanClassloader, BeanDefinition beanDefinition, String beanName) throws Exception
{
Class<?> beanClass = validateBeanClass(beanClassloader, beanDefinition, beanName);
validateBeanConstructor(beanDefinition, beanName, beanClass);
validateBeanSetters(beanDefinition, beanName, beanClass);
}
private Class<?> validateBeanClass(ClassLoader beanClassloader, BeanDefinition beanDefinition, String beanName) throws Exception
{
Class<?> beanClass;
try
{
beanClass = beanClassloader.loadClass(beanDefinition.getBeanClassName());
}
catch (ClassNotFoundException e)
{
throw new ClassNotFoundException("Cannot find " + beanDefinition.getBeanClassName() +
" for bean '" + beanName + "' in " + beanDefinition.getResourceDescription(), e);
}
return beanClass;
}
private void validateBeanConstructor(BeanDefinition beanDefinition, String beanName,
Class<?> beanClass) throws Exception
{
boolean foundConstructor = false;
ConstructorArgumentValues constructorArgs = beanDefinition.getConstructorArgumentValues();
Class<?>[] argTypes = null;
if (constructorArgs != null)
{
Constructor<?>[] constructors = beanClass.getDeclaredConstructors();
int suppliedArgCount = constructorArgs.getArgumentCount();
boolean isGenericArgs = !constructorArgs.getGenericArgumentValues().isEmpty();
for (int k = 0; k < constructors.length && !foundConstructor; k++)
{
Constructor<?> c = constructors[k];
knownConstructorLoop:
{
Class<?>[] knownConstructorsArgTypes = c.getParameterTypes();
if (knownConstructorsArgTypes.length == suppliedArgCount)
{
if (isGenericArgs)
{
foundConstructor = true; // TODO - support generic arg checking
}
else
{
for (int i = 0; i < knownConstructorsArgTypes.length; i++)
{
Class<?> argType = knownConstructorsArgTypes[i];
ConstructorArgumentValues.ValueHolder valHolder = constructorArgs.getArgumentValue(i,
argType);
if (valHolder == null)
{
break knownConstructorLoop;
}
}
foundConstructor = true;
}
}
}
}
}
else
{
try
{
Constructor c = beanClass.getConstructor(argTypes);
foundConstructor = true;
}
catch (Exception ignored) { }
}
if (!foundConstructor)
{
throw new NoSuchMethodException("No matching constructor could be found for bean '" +
beanName + "' for " + beanClass.toString() + " in " + beanDefinition.getResourceDescription());
}
}
private void validateBeanSetters(BeanDefinition beanDefinition, String beanName, Class<?> beanClass) throws Exception
{
MutablePropertyValues properties = beanDefinition.getPropertyValues();
List<PropertyValue> propList = properties.getPropertyValueList();
try
{
Method[] methods = beanClass.getMethods();
for (PropertyValue p : propList)
{
boolean foundMethod = false;
String propName = p.getName();
String setterMethodName = "set" + propName.substring(0, 1).toUpperCase();
if (propName.length() > 1)
{
setterMethodName += propName.substring(1);
}
for (int i = 0; i < methods.length && !foundMethod; i++)
{
Method m = methods[i];
foundMethod = m.getName().equals(setterMethodName);
}
if (!foundMethod)
{
throw new NoSuchMethodException("No matching setter method " + setterMethodName
+ " could be found for bean '" + beanName + "' for " + beanClass.toString() +
" in " + beanDefinition.getResourceDescription());
}
}
}
catch (NoClassDefFoundError e)
{
getLog().warn("Could not validate setter methods for bean " + beanName +
" since getting the methods of " + beanClass + " threw a NoClassDefFoundError: "
+ e.getLocalizedMessage());
}
}
private Collection<? extends URL> getUrlsForExtension(File file, String... extensions) throws Exception
{
Set<URL> ret = new HashSet<URL>();
if (file.isDirectory())
{
for (File childFile : file.listFiles())
{
ret.addAll(getUrlsForExtension(childFile, extensions));
}
}
else
{
for (String ex : extensions)
{
if (file.getName().endsWith("." + ex))
{
ret.add(file.toURI().toURL());
break;
}
}
}
return ret;
}
}
And the plugin's pom.xml:
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
... <my project's parent> ...
</parent>
<groupId>myplugins</groupId>
<artifactId>maven-spring-validation-plugin</artifactId>
<version>1.0</version>
<packaging>maven-plugin</packaging>
<name>Maven Spring Validation Plugin</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
</dependencies>
</project>
Once installed, run like so at the root level of your WAR module:
mvn myplugins:maven-spring-validation-plugin:validate