I'm building a webapp modelled on the MVC architecture. Form data from a JSP page gets forwarded to a Dispatcher Servlet which delegates to the Controller. I've managed to inject a single controller and it works just fine.
For multiple controllers I've created a Mapping Handler in the servlet which maps the request to the relevant Controller by reading in a properties file. I've injected the Mapping Handler but I'm still getting a NullPointerException.
An additional ControllerBundle just abstracts the Controller and method name into one object. I'm using tomee webprofile 1.7.4
DispatcherServlet.java:
package a1;
// imports
public class DispatcherServlet extends HttpServlet {
#Inject
#Named("MappingHandler")
private MappingHandler MAPPING_HANDLER;
// ** Single controller **
// #Inject
// #Named("customerController")
// private CustomerController controller;
#Override
public void init() throws ServletException {
MAPPING_HANDLER.generate();
}
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String pathInfo = req.getPathInfo();
Controller controller = MAPPING_HANDLER.getController(pathInfo);
String methodName = MAPPING_HANDLER.getMethod(pathInfo);
Method method = controller.getClass().getDeclaredMethod(methodName, HttpServletRequest.class);
method.setAccessible(true);
method.invoke(controller, req);
req.getRequestDispatcher("/results.jsp").forward(req, resp);
// ** Single controller **
// String methodName = "addCustomer";
// Method method = controller.getClass().getDeclaredMethod(methodName, HttpServletRequest.class);
//
// method.setAccessible(true);
// method.invoke(controller, req);
// req.getRequestDispatcher("/success.jsp").forward(req, resp);
} catch (SecurityException | IllegalArgumentException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
MappingHandler.java:
package a1;
// imports
#Named("MappingHandler")
public class MappingHandler {
private static final Map<String, ControllerBundle> CONTROLLER_BUNDLE_MAP = new HashMap<>();
private static final String PACK = "a1.";
void generate() {
try {
Properties props = new Properties();
InputStream is1 = this.getClass().getClassLoader().getResourceAsStream("mappings_h6.properties");
props.load(is1);
Enumeration e = props.propertyNames();
while (e.hasMoreElements()) {
String left = (String) e.nextElement();
String right = props.getProperty(left);
// ControllerBundle: abstraction containing both the Controller and the name of the method
ControllerBundle controllerBundle = new ControllerBundle(right, PACK);
CONTROLLER_BUNDLE_MAP.put(left, controllerBundle);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Controller getController(String pathInfo) {
ControllerBundle controllerBundle = CONTROLLER_BUNDLE_MAP.get(pathInfo);
Controller controller = controllerBundle.getController();
return controller;
}
String getMethod(String pathInfo) {
ControllerBundle controllerEntity1 = CONTROLLER_BUNDLE_MAP.get(pathInfo);
String methodName = controllerEntity1.getMethodName();
return methodName;
}
}
ControllerBundle.java:
package a1;
// imports
public class ControllerBundle {
private Controller controller;
private String methodName;
private static final Map<String, Controller> CONTROLLER_MAP = new HashMap<>();
public ControllerBundle(String value, String pack) {
String[] valueSplit = value.split("#");
String controllerName = valueSplit[0];
Controller controller = findController(pack, controllerName);
String methodName = valueSplit[1];
this.controller = controller;
this.methodName = methodName;
}
// Keep only one copy of every controller
private static Controller findController(String pack, String controllerName) {
try {
Class currentControllerClass = Class.forName(pack + controllerName);
for (String s : CONTROLLER_MAP.keySet()) {
Class existingControllerClass = CONTROLLER_MAP.get(s).getClass();
if (currentControllerClass == existingControllerClass) {
return (Controller) existingControllerClass.newInstance();
}
}
Controller currentController = (Controller) currentControllerClass.newInstance();
CONTROLLER_MAP.put(controllerName, currentController);
return currentController;
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Controller getController() {
return controller;
}
String getMethodName() {
return methodName;
}
}
mappings_h6.properties:
/customer/add=CustomerController#addCustomer
/customer/update=CustomerController#updateCustomer
/account/create=AccountController#createAccount
/account/freeze=AccountController#freezeAccount
beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
do you have a beans.xml in WEB-INF or WEB-INF/classes/META-INF? Otherwise on tomee 1.x CDI is not activated.
Related
I have the requirement to display a document in excel format in my spring mvc application. I have added excelViewResolver to my AppConfig but unable to process the excel file. Please help.
Following is the AppConfig class
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = "com.testapps.myapps")
public class AppConfig extends WebMvcConfigurerAdapter {
private static final String autowire = null;
/*
* Configure ContentNegotiationManager
*/
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.ignoreAcceptHeader(true).defaultContentType(
MediaType.TEXT_HTML);
}
/*
* Configure ContentNegotiatingViewResolver
*/
#Bean
public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(manager);
// Define all possible view resolvers
List<ViewResolver> resolvers = new ArrayList<ViewResolver>();
resolvers.add(jspViewResolver());
resolvers.add(excelViewResolver());
resolver.setViewResolvers(resolvers);
return resolver;
}
/*
* Configure View resolver to provide XLS output using Apache POI library to
* generate XLS output for an object content
*/
#Bean(name="excelView")
public ViewResolver excelViewResolver() {
System.out.println("inside view resolver for excel");
return new ExcelViewResolver();
}
/*
* Configure View resolver to provide HTML output This is the default format
* in absence of any type suffix.
*/
#Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
#Bean(name = "multipartResolver")
public StandardServletMultipartResolver resolver() {
return new StandardServletMultipartResolver();
}
}
WebAppInitializer.java
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { AppConfig.class };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
#Override
protected String[] getServletMappings() {
return new String[] { "*.html" };
}
#Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(getMultipartConfigElement());
}
private MultipartConfigElement getMultipartConfigElement() {
MultipartConfigElement multipartConfigElement = new MultipartConfigElement( LOCATION, MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
return multipartConfigElement;
}
private static final String LOCATION = "C:/TESTdevelopments/files/"; // Temporary location where files will be stored
private static final long MAX_FILE_SIZE = 5242880; // 5MB : Max file size.
// Beyond that size spring will throw exception.
private static final long MAX_REQUEST_SIZE = 20971520; // 20MB : Total request size containing Multi part.
private static final int FILE_SIZE_THRESHOLD = 0; // Size threshold after which files will be written to disk
}
ExcelBuilder.java
public class ExcelBuilder extends AbstractXlsxView {
private static final DateFormat DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT);
#Override
protected void buildExcelDocument(Map<String, Object> model,
Workbook workbook,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// change the file name
response.setHeader("Content-Disposition", "attachment; filename=\"my-xlsx-file.xlsx\"");
#SuppressWarnings("unchecked")
List<Course> courses = (List<Course>) model.get("courses");
// create excel xls sheet
Sheet sheet = workbook.createSheet("Spring MVC AbstractXlsxView");
// create header row
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("ID");
header.createCell(1).setCellValue("Name");
header.createCell(2).setCellValue("Date");
// Create data cells
int rowCount = 1;
for (Course course : courses){
Row courseRow = sheet.createRow(rowCount++);
courseRow.createCell(0).setCellValue(course.getId());
courseRow.createCell(1).setCellValue(course.getName());
courseRow.createCell(2).setCellValue(DATE_FORMAT.format(course.getDate()));
}
}
}
ExcelViewResolver.java
public class ExcelViewResolver implements ViewResolver{
#Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
ExcelBuilder view = new ExcelBuilder();
return view;
}
}
and finally controller class mapping...
#RequestMapping(value = "/downloadExcel.html", method = RequestMethod.GET)
public ModelAndView downloadExcel() {
// create some sample data
List<courseRecordParams> reports = courseRegisterService.generateReportsGeneral();
// return a view which will be resolved by an excel view resolver
return new ModelAndView("excelView", "reports", reports);
}
The problem is I am not able to view the excel file. Its generating a file not found error as in JBWEB000124: The requested resource is not available. since it tries to open a nonexisting jsp.
Please help.
Thanks in advance.
Found a solution to this problem.
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorPathExtension(true)
.favorParameter(false)
.ignoreAcceptHeader(true)
.useJaf(false)
.defaultContentType(MediaType.APPLICATION_JSON);
}
Also, I removed the excelviewresolver which was unnecessary since this is already handled by multipartviewresolver.
After this the excel file was getting generated without any problem.
i'm currently have a look a springboot undertow and it's not really clear (for me) how to dispatch an incoming http request to a worker thread for blocking operation handling.
Looking at the class UndertowEmbeddedServletContainer.class, it look like there is no way to have this behaviour since the only HttpHandler is a ServletHandler, that allow #Controller configurations
private Undertow createUndertowServer() {
try {
HttpHandler servletHandler = this.manager.start();
this.builder.setHandler(getContextHandler(servletHandler));
return this.builder.build();
}
catch (ServletException ex) {
throw new EmbeddedServletContainerException(
"Unable to start embdedded Undertow", ex);
}
}
private HttpHandler getContextHandler(HttpHandler servletHandler) {
if (StringUtils.isEmpty(this.contextPath)) {
return servletHandler;
}
return Handlers.path().addPrefixPath(this.contextPath, servletHandler);
}
By default, in undertow all requests are handled by IO-Thread for non blocking operations.
Does this mean that every #Controller executions will be processed by a non blocking thread ? or is there a solution to chose from IO-THREAD or WORKER-THREAD ?
I try to write a workaround, but this code is pretty uggly, and maybe someone has a better solution:
BlockingHandler.class
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface BlockingHandler {
String contextPath() default "/";
}
UndertowInitializer.class
public class UndertowInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
configurableApplicationContext.addBeanFactoryPostProcessor(new UndertowHandlerPostProcessor());
}
}
UndertowHandlerPostProcessor.class
public class UndertowHandlerPostProcessor implements BeanDefinitionRegistryPostProcessor {
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(BlockingHandler.class));
for (BeanDefinition beanDefinition : scanner.findCandidateComponents("org.me.lah")){
try{
Class clazz = Class.forName(beanDefinition.getBeanClassName());
beanDefinitionRegistry.registerBeanDefinition(clazz.getSimpleName(), beanDefinition);
} catch (ClassNotFoundException e) {
throw new BeanCreationException(format("Unable to create bean %s", beanDefinition.getBeanClassName()), e);
}
}
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
//no need to post process defined bean
}
}
override UndertowEmbeddedServletContainerFactory.class
public class UndertowEmbeddedServletContainerFactory extends AbstractEmbeddedServletContainerFactory implements ResourceLoaderAware, ApplicationContextAware {
private ApplicationContext applicationContext;
#Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
DeploymentManager manager = createDeploymentManager(initializers);
int port = getPort();
if (port == 0) {
port = SocketUtils.findAvailableTcpPort(40000);
}
Undertow.Builder builder = createBuilder(port);
Map<String, Object> handlers = applicationContext.getBeansWithAnnotation(BlockingHandler.class);
return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(),
port, port >= 0, handlers);
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
...
override UndertowEmbeddedServletContainer.class
public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager,
String contextPath, int port, boolean autoStart, Map<String, Object> handlers) {
this.builder = builder;
this.manager = manager;
this.contextPath = contextPath;
this.port = port;
this.autoStart = autoStart;
this.handlers = handlers;
}
private Undertow createUndertowServer() {
try {
HttpHandler servletHandler = this.manager.start();
String path = this.contextPath.isEmpty() ? "/" : this.contextPath;
PathHandler pathHandler = Handlers.path().addPrefixPath(path, servletHandler);
for(Entry<String, Object> entry : handlers.entrySet()){
Annotation annotation = entry.getValue().getClass().getDeclaredAnnotation(BlockingHandler.class);
System.out.println(((BlockingHandler) annotation).contextPath());
pathHandler.addPrefixPath(((BlockingHandler) annotation).contextPath(), (HttpHandler) entry.getValue());
}
this.builder.setHandler(pathHandler);
return this.builder.build();
}
catch (ServletException ex) {
throw new EmbeddedServletContainerException(
"Unable to start embdedded Undertow", ex);
}
}
set initializer to the application context
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).initializers(new UndertowInitializer()).run(args);
}
finaly create a HttpHandler that dispatch to worker thread
#BlockingHandler(contextPath = "/blocking/test")
public class DatabaseHandler implements HttpHandler {
#Autowired
private EchoService echoService;
#Override
public void handleRequest(HttpServerExchange httpServerExchange) throws Exception {
if(httpServerExchange.isInIoThread()){
httpServerExchange.dispatch();
}
echoService.getMessage("my message");
}
}
As you can see, my "solution" is really heavy, and i would really appreciate any help to simplify it a lot.
Thank you
You don't need to do anything.
Spring Boot's default Undertow configuration uses Undertow's ServletInitialHandler in front of Spring MVC's DispatcherServlet. This handler performs the exchange.isInIoThread() check and calls dispatch() if necessary.
If you place a breakpoint in your #Controller, you'll see that it's called on a thread named XNIO-1 task-n which is a worker thread (the IO threads are named XNIO-1 I/O-n).
I have a simple resource like:
#Path("/")
public class RootResource {
#Context WebConfig wc;
#PostConstruct
public void init() {
assertNotNull(wc);
}
#GET
public void String method() {
return "Hello\n";
}
}
Which I am trying to use with JerseyTest (2.x, not 1.x) and the GrizzlyTestContainerFactory.
I can't work out what I need to do in terms of config to get the WebConfig object injected.
I solved this issue by creating a subclass of GrizzlyTestContainerFactory and explicitly loading the Jersey servlet. This triggers the injection of the WebConfig object. The code looks like this:
public class ExtendedGrizzlyTestContainerFactory implements TestContainerFactory {
private static class GrizzlyTestContainer implements TestContainer {
private final URI uri;
private final ApplicationHandler appHandler;
private HttpServer server;
private static final Logger LOGGER = Logger.getLogger(GrizzlyTestContainer.class.getName());
private GrizzlyTestContainer(URI uri, ApplicationHandler appHandler) {
this.appHandler = appHandler;
this.uri = uri;
}
#Override
public ClientConfig getClientConfig() {
return null;
}
#Override
public URI getBaseUri() {
return uri;
}
#Override
public void start() {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Starting GrizzlyTestContainer...");
}
try {
this.server = GrizzlyHttpServerFactory.createHttpServer(uri, appHandler);
// Initialize and register Jersey Servlet
WebappContext context = new WebappContext("WebappContext", "");
ServletRegistration registration = context.addServlet("ServletContainer", ServletContainer.class);
registration.setInitParameter("javax.ws.rs.Application",
appHandler.getConfiguration().getApplication().getClass().getName());
// Add an init parameter - this could be loaded from a parameter in the constructor
registration.setInitParameter("myparam", "myvalue");
registration.addMapping("/*");
context.deploy(server);
} catch (ProcessingException e) {
throw new TestContainerException(e);
}
}
#Override
public void stop() {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Stopping GrizzlyTestContainer...");
}
this.server.stop();
}
}
#Override
public TestContainer create(URI baseUri, ApplicationHandler application) throws IllegalArgumentException {
return new GrizzlyTestContainer(baseUri, application);
}
Notice that the Jersey Servlet configuration is being loaded from the ApplicationHandler that is passed in as a parameter using the inner Application object's class name (ResourceConfig is a subclass of Application). Therefore, you also need to create a subclass of ResourceConfig for this approach to work. The code for this is very simple:
package com.example;
import org.glassfish.jersey.server.ResourceConfig;
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
super(MyResource.class);
}
}
This assumes the resource you are testing is MyResource. You also need to override a couple of methods in your test like this:
public class MyResourceTest extends JerseyTest {
public MyResourceTest() throws TestContainerException {
}
#Override
protected Application configure() {
return new MyResourceConfig();
}
#Override
protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
return new ExtendedGrizzlyTestContainerFactory();
}
#Test
public void testCreateSimpleBean() {
final String beanList = target("test").request().get(String.class);
Assert.assertNotNull(beanList);
}
}
Finally, for completeness, here is the code for MyResource:
#Path("test")
public class MyResource {
#Context WebConfig wc;
#PostConstruct
public void init() {
System.out.println("WebConfig: " + wc);
String url = wc.getInitParameter("myparam");
System.out.println("myparam = "+url);
}
#GET
#Produces(MediaType.APPLICATION_JSON)
public Collection<TestBean> createSimpleBean() {
Collection<TestBean> res = new ArrayList<TestBean>();
res.add(new TestBean("a", 1, 1L));
res.add(new TestBean("b", 2, 2L));
return res;
}
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public TestBean roundTrip(TestBean s) {
return s;
}
}
The output of running the test shows that the WebConfig is loaded and the init param is now available:
WebConfig: org.glassfish.jersey.servlet.WebServletConfig#107d0f44
myparam = myvalue
The solution from #ametke worked well but wasn't picking up my ExceptionMapper classes. To solve this I simplified the start() method to:
#Override
public void start() {
try {
initParams.put("jersey.config.server.provider.packages", "my.resources;my.config");
this.server = GrizzlyWebContainerFactory.create(uri, initParams);
} catch (ProcessingException | IOException e) {
throw new TestContainerException(e);
}
}
This was based on Problems running JerseyTest when dealing with HttpServletResponse
Every time I try to open a file at the #Timeout, Java returns Null Pointer Exception
#Singleton
public class EngineTrans {
#Resource
private TimerService timerService;
public void createProgrammaticalTimer() {
ScheduleExpression everyTenSeconds = new ScheduleExpression().second("*/15").minute("*").hour("4-20");
timerService.createCalendarTimer(everyTenSeconds, new TimerConfig(
"passed message " + new Date(), false));
}
#Timeout
public void handleTimer() {
System.out.println("timer received - contained message is: " + new Date());
File xmlFile = new File(FacesContext.getCurrentInstance().getExternalContext().getRealPath(""));
}
}
Any ideas?
There is no JSF context within an #Timeout method. Perform the getRealPath call in the createProgrammaticalTimer method, and then pass it to the #Timeout method via the first parameter of the TimerConfig constructor (the "info" parameter). If necessary, create an inner class to hold all the data you need to pass to the #Timeout method:
#Singleton
public class EngineTrans {
#Resource
private TimerService timerService;
private static class TimeoutData {
private final Date date = new Date();
private final String resourcePath;
...
}
public void createProgrammaticalTimer() {
...
String resourcePath = FacesContext.getCurrentInstance().getExternalContext().getRealPath("...");
TimeoutData timeoutData = new TimeoutData(resourcePath);
timerService.createCalendarTimer(everyTenSeconds, new TimerConfig(timeoutData, false));
}
#Timeout
public void handleTimer(Timer timer) {
TimeoutData timeoutData = (TimeoutData)timer.getInfo();
...
}
}
My problem is to how to call this. I could do
MyObject o = new MyObject();
myController.save(o, "value");
but this is not what I would like to do. I would like the MyObject to be in the request post body? How can this be done?
#Requestmapping(value="/save/{value}", method=RequestMethod.POST)
public void post(#Valid MyObject o, #PathVariable String value{
objectService.save(o);
}
Just to be clear I am talking about unit testing.
Edit:
#RequestMapping(value = "/", method = RequestMethod.POST)
public View postUser(ModelMap data, #Valid Profile profile, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return dummyDataView;
}
data.put(DummyDataView.DATA_TO_SEND, "users/user-1.json");
profileService.save(profile);
return dummyDataView;
}
See sample code below that demonstrates unit testing a controller using junit and spring-test.
#RunWith(SpringJUnit4ClassRunner.class)
#TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class })
#Transactional
#ContextConfiguration(locations = {
"classpath:rest.xml"
})
public class ControllerTest{
private MockHttpServletRequest request;
private MockHttpServletResponse response;
#Autowired
private RequestMappingHandlerAdapter handlerAdapter;
#Autowired
private RequestMappingHandlerMapping handlerMapping;
#Before
public void setUp() throws Exception
{
this.request = new MockHttpServletRequest();
request.setContentType("application/json");
this.response = new MockHttpServletResponse();
}
#Test
public void testPost(){
request.setMethod("POST");
request.setRequestURI("/save/test"); //replace test with any value
final ModelAndView mav;
Object handler;
try{
MyObject o = new MyObject();
//set values
//Assuming the controller consumes json
ObjectMapper mapper = new ObjectMapper();
//set o converted as JSON to the request body
//request.setContent(mapper.writeValueAsString(o).getBytes());
request.setAttribute("attribute_name", o); //in case you are trying to set a model attribute.
handler = handlerMapping.getHandler(request).getHandler();
mav = handlerAdapter.handle(request, response, handler);
Assert.assertEquals(200, response.getStatus());
//Assert other conditions.
}
catch (Exception e)
{
}
}
}
You need to use RequestBody:
#Requestmapping(value="/save/{value}", method=RequestMethod.POST)
public void post(#RequestBody MyObject o, #PathVariable String value{
objectService.save(o);
}
general info about request body documentation : http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html#mvc-ann-requestbody