JSF View scope in Spring - spring

Is there any scope like JSF #ViewScoped in Spring 3.0? I have an application using JSF+Spring where backing beans are managed by Spring. I didn't find any scope like JSF wiew scope in Spring. I saw the blog Porting JSF 2.0’s ViewScope to Spring 3.0, but it didn't work for me.
Here's my attempt on the custom Spring scope:
import java.util.Map;
import javax.faces.context.FacesContext;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
/**
* Implements the JSF View Scope for use by Spring. This class is registered as a Spring bean with the CustomScopeConfigurer.
*/
public class ViewScope implements Scope {
public Object get(String name, ObjectFactory<?> objectFactory) {
System.out.println("**************************************************");
System.out.println("-------------------- Getting objects For View Scope ----------");
System.out.println("**************************************************");
if (FacesContext.getCurrentInstance().getViewRoot() != null) {
Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
if (viewMap.containsKey(name)) {
return viewMap.get(name);
} else {
Object object = objectFactory.getObject();
viewMap.put(name, object);
return object;
}
} else {
return null;
}
}
public Object remove(String name) {
System.out.println("**************************************************");
System.out.println("-------------------- View Scope object Removed ----------");
System.out.println("**************************************************");
if (FacesContext.getCurrentInstance().getViewRoot() != null) {
return FacesContext.getCurrentInstance().getViewRoot().getViewMap().remove(name);
} else {
return null;
}
}
public void registerDestructionCallback(String name, Runnable callback) {
// Do nothing
}
public Object resolveContextualObject(String key) { return null;
}
public String getConversationId() {
return null;
}
}
application-context.xml:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="view">
<bean class="com.delta.beans.ViewScope"/>
</entry>
</map>
</property>
</bean>

Recently I've created maven artifact which will solve this problem.
See my github javaplugs/spring-jsf repository.

I did something like this without Porting bean to Spring. It's working for me.
#ManagedBean(name="bean")
#ViewScoped // actual jsf viewscoped only with javax.faces.viewscoped import
public class Bean implements
Serializable {
#ManagedProperty(value="#{appService}") // Spring Manged Bean and singleton
private transient AppService appService;
// Getting AppService Object which is singleton in the application during deserialization
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
FacesContext context = FacesContext.getCurrentInstance();
appService = (AppService)context.getApplication()
.evaluateExpressionGet(context, "#{appService}", AppService.class);
}
}

public class ViewScopeCallbackRegistrer implements ViewMapListener {
#SuppressWarnings("unchecked")
#Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
if (event instanceof PostConstructViewMapEvent) {
PostConstructViewMapEvent viewMapEvent = (PostConstructViewMapEvent) event;
UIViewRoot viewRoot = (UIViewRoot) viewMapEvent.getComponent();
viewRoot.getViewMap().put(
ViewScope.VIEW_SCOPE_CALLBACKS,
new HashMap<String, Runnable>()
);
} else if (event instanceof PreDestroyViewMapEvent) {
PreDestroyViewMapEvent viewMapEvent = (PreDestroyViewMapEvent) event;
UIViewRoot viewRoot = (UIViewRoot) viewMapEvent.getComponent();
Map<String, Runnable> callbacks = (Map<String, Runnable>) viewRoot
.getViewMap().get(ViewScope.VIEW_SCOPE_CALLBACKS);
if (callbacks != null) {
for (Runnable c : callbacks.values()) {
c.run();
}
callbacks.clear();
}
}
}
#Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
}

public class ViewScope implements Scope {
public static final String VIEW_SCOPE_CALLBACKS = "viewScope.callbacks";
#Override
public synchronized Object get(String name, ObjectFactory<?> objectFactory) {
Object instance = this.getViewMap().get(name);
if(instance == null){
instance = objectFactory.getObject();
this.getViewMap().put(name, instance);
}
return instance;
}
#SuppressWarnings("unchecked")
#Override
public Object remove(String name) {
Object instance = this.getViewMap().remove(name);
if(instance == null){
Map<String, Runnable> callbacks = (Map<String, Runnable>) this.getViewMap().get(VIEW_SCOPE_CALLBACKS);
if(callbacks != null)
callbacks.remove(name);
}
return instance;
}
/**
* Responsável por registrar uma chamada de destruição ao bean
* que será armazenadano [b]viewMap[/b] da [b]ViewRoot[/b](nossa página que será mostrada)
* #see #getViewMap()
* #param name - nome do bean
* #param runnable
*/
#SuppressWarnings("unchecked")
#Override
public void registerDestructionCallback(String name, Runnable runnable) {
Map<String, Runnable> callbacks = (Map<String, Runnable>) this.getViewMap().get(VIEW_SCOPE_CALLBACKS);
if(callbacks != null)
callbacks.put(name, runnable);
}
#Override
public Object resolveContextualObject(String key) {
FacesContext facesContext = FacesContext.getCurrentInstance();
FacesRequestAttributes facesResquestAttributes = new FacesRequestAttributes(facesContext);
return facesResquestAttributes.resolveReference(key);
}
#Override
public String getConversationId() {
FacesContext facesContext = FacesContext.getCurrentInstance();
FacesRequestAttributes facesResquestAttributes = new FacesRequestAttributes(facesContext);
return facesResquestAttributes.getSessionId() + "-" + facesContext.getViewRoot().getViewId();
}
private Map<String, Object> getViewMap(){
return FacesContext.getCurrentInstance().getViewRoot().getViewMap();
}
}

I have tried a work around for the Jsf view bean memory leak issue for both Jsf 2.1 & Jsf 2.2. Try the code in following link Memory leak with ViewScoped bean?. It will clear the view bean in session while navigating to next page.

Related

JavaFX custom controls created with a Builder and binding expressions

I’m using Spring together with JavaFx. To use spring bean as a custom control I need to use BuilderFactory and a Builder to get a bean from the context. Otherwice I don't have an application context
Parent.java
#Component
#Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ParentControl extends VBox {
#Autowired
ControlFXMLLoader controlFXMLLoader;
#Value("classpath:/parent.fxml")
private Resource fxml;
#PostConstruct
void load() throws IOException {
controlFXMLLoader.load(fxml.getURL(), this);
}
public ParentControl() {
//no application context
}
public LocalDate getDate() {
return LocalDate.now();
}
}
BeanBuilderFactory.java
#Component
public class BeanBuilderFactory implements BuilderFactory {
private Logger logger = LogManager.getLogger(BeanBuilderFactory.class);
#Autowired
private ConfigurableApplicationContext context;
public BeanBuilderFactory() {
}
private JavaFXBuilderFactory defaultBuilderFactory = new JavaFXBuilderFactory();
#Override
public Builder<?> getBuilder(Class<?> type) {
try {
String[] beanNames = context.getBeanNamesForType(type);
if (beanNames.length == 1) {
return new Builder<Object>() {
#Override
public Object build() {
return context.getBean(beanNames[0]);
}
};
} else {
return defaultBuilderFactory.getBuilder(type);
}
} catch (BeansException e) {
return defaultBuilderFactory.getBuilder(type);
}
}
}
And then I user this BuilderFactory to load fxml for a custom control
ControlFXMLLoader.java
#Component
public class ControlFXMLLoader {
private Logger logger = LogManager.getLogger(ControlFXMLLoader.class);
#Autowired
protected ConfigurableApplicationContext context;
#Autowired
protected BeanBuilderFactory beanBuilderFactory;
public Object load(URL fxmlUrl, Parent root, Object controller) throws IOException {
logger.debug("load");
javafx.fxml.FXMLLoader loader = new javafx.fxml.FXMLLoader(fxmlUrl);
loader.setControllerFactory(context::getBean);
loader.setBuilderFactory(beanBuilderFactory);
loader.setRoot(root);
loader.setController(controller);
return loader.load();
}
public Object load(URL fxmlUrl, Parent root) throws IOException {
return load(fxmlUrl, root, root);
}
}
Now I have a child custom control
ChildControl.java
#Component
#Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ChildControl extends VBox {
public ChildControl() {
}
#Autowired
ControlFXMLLoader controlFXMLLoader;
#Value("classpath:/child.fxml")
private Resource fxml;
#PostConstruct
void load() throws IOException {
controlFXMLLoader.load(fxml.getURL(), this);
}
ObjectProperty<LocalDate> date = new SimpleObjectProperty<LocalDate>();
public LocalDate getDate() {
return date.get();
}
public void setDate(LocalDate date) {
this.date.set(date);
}
public ObjectProperty<LocalDate> dateProperty() {
return date;
}
#FXML
protected void doSomething() {
System.out.println("The button was clicked! " + date.get().toString());
}
}
And want to assign the date to the child from parent fxml
parent.fxml
<fx:root type="com.example.javafx.ParentControl" xmlns:fx="http://javafx.com/fxml">
<ChildControl date="${controller.date}"/>
</fx:root>
child.fxml
<fx:root type="com.example.javafx.ChildControl" xmlns:fx="http://javafx.com/fxml">
<TextField fx:id="textField"/>
<Button text="Click Me" onAction="#doSomething"/>
</fx:root>
The problem is that FXMLLoader doesn’t not allow to use Binding Expression together with a Builder. I got "Cannot bind to builder property." exception.
Below is the part of the code from FXMLLoader.java and the very last if that causes the problem.
Is there some other solution?
FXMLLoader.java
public void processPropertyAttribute(Attribute attribute) throws IOException {
String value = attribute.value;
if (isBindingExpression(value)) {
// Resolve the expression
Expression expression;
if (attribute.sourceType != null) {
throw constructLoadException("Cannot bind to static property.");
}
if (!isTyped()) {
throw constructLoadException("Cannot bind to untyped object.");
}
// TODO We may want to identify binding properties in processAttribute()
// and apply them after build() has been called
if (this.value instanceof Builder) {
throw constructLoadException("Cannot bind to builder property.");
}

No thread-bound request found with Spring when Kafka receive a message

I 'm getting this error from my service
jvm org.hibernate.internal.ExceptionMapperStandardImpl {"#trace_info":"[availability-psql,eba16d49e23479cc,675789f41e0dda5b,eba16d49e23479cc,false]", "#message": "HHH000346: Error during managed flush [Error creating bean with name 'scopedTarget.infoUser': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.]
This is because of I have a bean of scope #ScopeRequest. This problem show up when a new message from kafka is received and I try to update my data base with spring data. If I remove my #Transactional I don't have any problem to save the data.
#KafkaListener(topics = "#{kafkaMastersConfig.topics}", containerFactory = "mastersContainerFactory")
#Transactional
#Authorized
public void consumeWrapperMasterChangeEvent(#Payload String payload,
#Header(KafkaHeaders.RECEIVED_TOPIC) String topic, #Nullable #Header(AUTHORIZATION) String authorization) throws IOException {
try {
log.info("Received change event in masters: '{}'", payload);
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
RequestContextHolder.setRequestAttributes(context);
changeProcessorFactory.getEntityChangeProcessor(getEntityFromTopic(topic)).processChange(payload);
} catch ( Exception e ) {
log.error("Error proccesing message {} ", e.getMessage());
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
And here is the bean:
#RequestScope
#Component
#NoArgsConstructor
#Getter
#Setter
public class InfoUser {
private DecodedJWT jwt;
public String getCurrentUser() {
if (jwt == null) {
return null;
}
return jwt.getSubject();
}
public String getAuthorizationBearer() {
if (jwt == null) {
return null;
}
return jwt.getToken();
}
}
And this class:
public class CustomRequestScopeAttr implements RequestAttributes {
private Map<String, Object> requestAttributeMap = new HashMap<>();
#Override
public Object getAttribute(String name, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.get(name);
}
return null;
}
#Override
public void setAttribute(String name, Object value, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
this.requestAttributeMap.put(name, value);
}
}
#Override
public void removeAttribute(String name, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
this.requestAttributeMap.remove(name);
}
}
#Override
public String[] getAttributeNames(int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.keySet().toArray(new String[0]);
}
return new String[0];
}
#Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
// Not Supported
}
#Override
public Object resolveReference(String key) {
// Not supported
return null;
}
#Override
public String getSessionId() {
return null;
}
#Override
public Object getSessionMutex() {
return null;
}
}
And futhermore I have an aspect class to save the authorization token:
#Aspect
#Component
#RequiredArgsConstructor
public class AuthorizationAspect {
private final AuthorizationDecoder authorizationDecoder;
private final ApplicationContext applicationContext;
#Around("#annotation(Authorized)")
public Object setInfoUser(ProceedingJoinPoint joinPoint) throws Throwable {
try {
String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Object[] args = joinPoint.getArgs();
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < args.length; i++) {
if (null != args[i]) {
arguments.put(parameterNames[i], args[i]);
}
}
Object authorization = arguments.get("authorization");
RequestContextHolder.setRequestAttributes(new CustomRequestScopeAttr());
InfoUser infoUser = applicationContext.getBean(InfoUser.class);
infoUser.setJwt(authorizationDecoder.decodeToken((String) authorization));
return joinPoint.proceed();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
And the last class is trying to save de info:
#RequiredArgsConstructor
public class RoomChangeMaster implements ChangeMaster<Room> {
private final TimetableRepository timetableRepository;
private final AvailabilityRepository availabilityRepository;
#Override
public void processChange(Room entity, ActionEnum action) {
if (action == ActionEnum.updated) {
List<Timetable> timetables = (List<Timetable>) timetableRepository.findByRoomId(entity.getId());
Room room = timetables.get(0).getRoom();
room.setDescription(entity.getDescription());
room.setCode(entity.getCode());
timetables.forEach(timetable -> {
timetable.setRoom(room);
timetableRepository.save(timetable);
});
availabilityRepository
.updateAvailabilityRoomByRoomId(room, entity.getId());
} else {
throw new IllegalStateException("Unexpected value: " + action);
}
}
}
I have spent a lot of time finding out the problem, but so far, I was not able to know the problem. Any idea will be appreciate.
Thank you
RequestContextHolder is for Spring-MVC - it is for a Web request only and is populated with information from an HTTP request.
/**
* Holder class to expose the web request in the form of a thread-bound
* {#link RequestAttributes} object. The request will be inherited
* by any child threads spawned by the current thread if the
* {#code inheritable} flag is set to {#code true}.
*
...
There is no equivalent for listener containers (of any type) because there is no "incoming request".
Looks like your hibernate code is tightly tied to the web.
If you are trying to reuse existing code you need to decouple it and use some other technique to pass information between the layers (e.g. a custom equivalent of RequestContextHolder).
Finally, I have solved it changing the hiberante method save by saveAndFlush

Service not Autowired in JSF Converter [duplicate]

This question already has answers here:
How to inject #EJB, #PersistenceContext, #Inject, #Autowired, etc in #FacesConverter?
(5 answers)
Closed 7 years ago.
I am working on a web application using PrimeFaces, JPA, Hibernate and JSF 2.0.
I have a converter for my JSF p:selectOneMenu. My problem is, when I run my application the Service descriptifService is not autowired, it return NULL !
The converter :
#Component
#FacesConverter(value = "descriptifConverter")
public class DescriptifConverter implements Converter {
#Autowired
#RmiClient
private IDescriptifService descriptifService;
#Override
public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2) {
if (arg2 == null || arg2.isEmpty()) {
return null;
}
String descriptif = arg2;
Long value = Long.valueOf(descriptif);
DescriptifDto result = new DescriptifDto();
result = descriptifService.findById(value);
return result;
}
#Override
public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2) {
if(arg2 == null || ((DescriptifDto) arg2).getIdDescriptif() == null) return null;
DescriptifDto descriptif = new DescriptifDto();
if(arg2 instanceof DescriptifDto) {
descriptif = (DescriptifDto) arg2;
String idDescriptif = descriptif.getIdDescriptif().toString();
return (idDescriptif != null) ? String.valueOf(idDescriptif) : null;
} else throw new ConverterException("Something wrong!" + arg2.hashCode() + arg2.toString());
}
}
The JSF code :
<p:selectOneMenu value="#{lotController.selectedDescriptif}"
effect="fade">
<f:selectItems value="#{lotController.listDescriptifs}" var="descriptif"
itemLabel="#{descriptif.libelle}" itemValue="#{descriptif}" />
<f:converter binding="#{descriptifConverter}" />
</p:selectOneMenu>
Here you have two options:
1 - Register a context provider bean:
AppContext Class:
import org.springframework.context.ApplicationContext;
public class AppContext {
private static ApplicationContext ctx;
public static void setApplicationContext(
ApplicationContext applicationContext) {
ctx = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
}
ApplicationContextProvider class:
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class ApplicationContextProvider implements ApplicationContextAware {
#Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
AppContext.setApplicationContext(applicationContext);
}
}
Register the bean:
<bean id="contextApplicationContextProvider" class="com.example.ApplicationContextProvider" />
Now, through the context, you can get a reference to your service bean anyware:
IDescriptifService descriptifService = AppContext.getApplicationContext().getBean(
IDescriptifService.class);
2 - Store the converted values inside the ViewMap (inspired in this post)
I like this solution because it doesn't required database access which improves the performance of the application.
AbstractConverter class
import java.util.HashMap;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
public abstract class AbstractConverter implements Converter {
private static final String KEY = "com.example.converters.AbstractConverter";
protected Map<String, Object> getViewMap(FacesContext context) {
Map<String, Object> viewMap = context.getViewRoot().getViewMap();
#SuppressWarnings({ "unchecked", "rawtypes" })
Map<String, Object> idMap = (Map) viewMap.get(KEY);
if (idMap == null) {
idMap = new HashMap<String, Object>();
viewMap.put(KEY, idMap);
}
return idMap;
}
#Override
public final Object getAsObject(FacesContext context, UIComponent c,
String value) {
if (value == null || value.isEmpty()) {
return null;
}
return getViewMap(context).get(value);
}
#Override
public final String getAsString(FacesContext context, UIComponent c,
Object value) {
if (value != null) {
String id = getConversionId(value);
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException(
"Objeto não pode ser convertido.");
}
getViewMap(context).put(id, value);
return id;
}
return null;
}
//Every concrete class must provide an unique conversionId String
//to every instance of the converted object
public abstract String getConversionId(Object value);
}
Here we create a storage place inside the ViewMap. We can now use it to store any converter object we need.
Here is an example of a concrete converter:
EntityConverter class
import javax.faces.convert.FacesConverter;
import com.example.AbstractEntity;
#FacesConverter("entity")
public class EntityConverter extends AbstractConverter {
#Override
public String getConversionId(Object value) {
if (value instanceof AbstractEntity) {
AbstractEntity entity = (AbstractEntity) value;
StringBuilder sb = new StringBuilder();
sb.append(entity.getClass().getSimpleName());
sb.append("#");
sb.append(entity.getId());
return sb.toString();
}
return null;
}
}
A late response, but I had the same problem of not being able to Autowire Spring beans into a JSF Converter, so I removed the #FacesConverter annotation and declared the converter component as session scoped, then, as you did, using the f:converter tag with binding attribute solved the problem. In your case:
#Component
#Scope(WebApplicationContext.SCOPE_SESSION)
public class DescriptifConverter implements Converter {
...
}
should work...

hibernate-envers RevisionListener spring integration as spring bean

I need to do some database processing in revision listener of hibernate-envers. For that I need inejction capabilities of Spring Framework. How can this be implemented? Here is the code representing the need but CustomRevisionListener is instantiated by a constructor in Envers code. Spring has only SecurityContextHolder as static service locator. How to inject other beans?
#Service
public class CustomRevisionListener implements EntityTrackingRevisionListener {
#Resource
private PersistenceManagerHibernate persistenceManagerHibernate;
public void newRevision(Object revisionEntity) {
CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity;
revision.setUsername(getUsername());
}
public String getUsername() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
if (context.getAuthentication() != null) {
return context.getAuthentication().getName();
} else {
return "anonymous";
}
}
return "anonymous";
}
#Override
public void entityChanged(#SuppressWarnings("rawtypes") Class entityClass, String entityName, Serializable entityId, RevisionType revisionType, Object revisionEntity) {
CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity;
revision.setEntityId(entityId.toString());
revision.setEntityName(entityName);
revision.setRevisionType((int)revisionType.getRepresentation());
Auditable auditable = null;
if (entityId instanceof Long) {
auditable = persistenceManagerHibernate.findById(entityClass, (Long)entityId);
}
revision.setGroupName(auditable.getAuditGroupName());
revision.setGroupEntityId(auditable.getAuditGroupId());
}
}
Since CustomRevisionListener is instantiated by a constructor in Envers, you have to find another way to retrieve a handle to a Spring managed bean.
You could create a utility class to achieve this:
/**
* Utility class which provides lookup services for Spring managed beans from
* within unmanaged beans.
*/
#Component
public class ContextLookup implements ApplicationContextAware {
private static ApplicationContext sApplicationContext;
#Override
public void setApplicationContext( ApplicationContext aApplicationContext )
throws BeansException {
setContext( aApplicationContext );
}
public static void setContext( ApplicationContext aApplicationContext ) {
sApplicationContext = aApplicationContext;
}
protected static ApplicationContext getApplicationContext() {
return sApplicationContext;
}
public static Object getBean( String aName ) {
if ( sApplicationContext != null ) {
return sApplicationContext.getBean( aName );
}
return null;
}
public static <T> T getBean( Class<T> aClass ) {
if ( sApplicationContext != null ) {
return sApplicationContext.getBean( aClass );
}
return null;
}
}
and in your CustomRevisionListener
public class CustomRevisionListener {
public void myMethod() {
..
// get a handle to your spring managed bean
Foo foo = (Foo)ContextLookup.getBean( "mySpringManagedBean" );
..
}
}

Spring MVC Annotated Controller Interface with #PathVariable

Is there any reason not to map Controllers as interfaces?
In all the examples and questions I see surrounding controllers, all are concrete classes. Is there a reason for this? I would like to separate the request mappings from the implementation. I hit a wall though when I tried to get a #PathVariable as a parameter in my concrete class.
My Controller interface looks like this:
#Controller
#RequestMapping("/services/goal/")
public interface GoalService {
#RequestMapping("options/")
#ResponseBody
Map<String, Long> getGoals();
#RequestMapping(value = "{id}/", method = RequestMethod.DELETE)
#ResponseBody
void removeGoal(#PathVariable String id);
}
And the implementing class:
#Component
public class GoalServiceImpl implements GoalService {
/* init code */
public Map<String, Long> getGoals() {
/* method code */
return map;
}
public void removeGoal(String id) {
Goal goal = goalDao.findByPrimaryKey(Long.parseLong(id));
goalDao.remove(goal);
}
}
The getGoals() method works great; the removeGoal(String id) throws an exception
ExceptionHandlerExceptionResolver - Resolving exception from handler [public void
todo.webapp.controllers.services.GoalServiceImpl.removeGoal(java.lang.String)]:
org.springframework.web.bind.MissingServletRequestParameterException: Required
String parameter 'id' is not present
If I add the #PathVariable annotation to the concrete class everything works as expected, but why should i have to re-declare this in the concrete class? Shouldn't it be handled by whatever has the #Controller annotation?
Apparently, when a request pattern is mapped to a method via the #RequestMapping annotation, it is mapped to to the concrete method implementation. So a request that matches the declaration will invoke GoalServiceImpl.removeGoal() directly rather than the method that originally declared the #RequestMapping ie GoalService.removeGoal().
Since an annotation on an interface, interface method, or interface method parameter does not carry over to the implementation there is no way for Spring MVC to recognize this as a #PathVariable unless the implementing class declares it explicitly. Without it, any AOP advice that targets #PathVariable parameters will not be executed.
The feature of defining all bindings on interface actually got implement recently in Spring 5.1.5.
Please see this issue: https://github.com/spring-projects/spring-framework/issues/15682 - it was a struggle :)
Now you can actually do:
#RequestMapping("/random")
public interface RandomDataController {
#RequestMapping(value = "/{type}", method = RequestMethod.GET)
#ResponseBody
RandomData getRandomData(
#PathVariable(value = "type") RandomDataType type, #RequestParam(value = "size", required = false, defaultValue = "10") int size);
}
#Controller
public class RandomDataImpl implements RandomDataController {
#Autowired
private RandomGenerator randomGenerator;
#Override
public RandomData getPathParamRandomData(RandomDataType type, int size) {
return randomGenerator.generateRandomData(type, size);
}
}
You can even use this library: https://github.com/ggeorgovassilis/spring-rest-invoker
To get a client-proxy based on that interface, similarly to how RestEasys client framework works in the JAX-RS land.
It works in newer version of Spring.
import org.springframework.web.bind.annotation.RequestMapping;
public interface TestApi {
#RequestMapping("/test")
public String test();
}
Implement the interface in the Controller
#RestController
#Slf4j
public class TestApiController implements TestApi {
#Override
public String test() {
log.info("In Test");
return "Value";
}
}
It can be used as:
Rest client
Recently I had the same problem. Following has worked for me:
public class GoalServiceImpl implements GoalService {
...
public void removeGoal(#PathVariableString id) {
}
}
i resolved this problem.
ON CLIENT SIDE:
I'm using this library https://github.com/ggeorgovassilis/spring-rest-invoker/. This library generate a proxy from interface to invoke spring rest service.
I extended this library:
I created an annotations and a factory client class:
Identify a Spring Rest Service
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface SpringRestService {
String baseUri();
}
This class generates a client rest from interfaces
public class RestFactory implements BeanFactoryPostProcessor,EmbeddedValueResolverAware {
StringValueResolver resolver;
#Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.resolver = resolver;
}
private String basePackage = "com";
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
createBeanProxy(beanFactory,SpringRestService.class);
createBeanProxy(beanFactory,JaxrsRestService.class);
}
private void createBeanProxy(ConfigurableListableBeanFactory beanFactory,Class<? extends Annotation> annotation) {
List<Class<Object>> classes;
try {
classes = AnnotationUtils.findAnnotatedClasses(basePackage, annotation);
} catch (Exception e) {
throw new BeanInstantiationException(annotation, e.getMessage(), e);
}
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (Class<Object> classType : classes) {
Annotation typeService = classType.getAnnotation(annotation);
GenericBeanDefinition beanDef = new GenericBeanDefinition();
beanDef.setBeanClass(getQueryServiceFactory(classType, typeService));
ConstructorArgumentValues cav = new ConstructorArgumentValues();
cav.addIndexedArgumentValue(0, classType);
cav.addIndexedArgumentValue(1, baseUri(classType,typeService));
beanDef.setConstructorArgumentValues(cav);
registry.registerBeanDefinition(classType.getName() + "Proxy", beanDef);
}
}
private String baseUri(Class<Object> c,Annotation typeService){
String baseUri = null;
if(typeService instanceof SpringRestService){
baseUri = ((SpringRestService)typeService).baseUri();
}else if(typeService instanceof JaxrsRestService){
baseUri = ((JaxrsRestService)typeService).baseUri();
}
if(baseUri!=null && !baseUri.isEmpty()){
return baseUri = resolver.resolveStringValue(baseUri);
}else{
throw new IllegalStateException("Impossibile individuare una baseUri per l'interface :"+c);
}
}
private static Class<? extends FactoryBean<?>> getQueryServiceFactory(Class<Object> c,Annotation typeService){
if(typeService instanceof SpringRestService){
return it.eng.rete2i.springjsonmapper.spring.SpringRestInvokerProxyFactoryBean.class;
}else if(typeService instanceof JaxrsRestService){
return it.eng.rete2i.springjsonmapper.jaxrs.JaxRsInvokerProxyFactoryBean.class;
}
throw new IllegalStateException("Impossibile individuare una classe per l'interface :"+c);
}
}
I configure my factory:
<bean class="it.eng.rete2i.springjsonmapper.factory.RestFactory">
<property name="basePackage" value="it.giancarlo.rest.services" />
</bean>
ON REST SERVICE SIGNATURE
this is an example interface:
package it.giancarlo.rest.services.spring;
import ...
#SpringRestService(baseUri="${bookservice.url}")
public interface BookService{
#Override
#RequestMapping("/volumes")
QueryResult findBooksByTitle(#RequestParam("q") String q);
#Override
#RequestMapping("/volumes/{id}")
Item findBookById(#PathVariable("id") String id);
}
ON REST SERVICE IMPLEMENTATION
Service implementation
#RestController
#RequestMapping("bookService")
public class BookServiceImpl implements BookService {
#Override
public QueryResult findBooksByTitle(String q) {
// TODO Auto-generated method stub
return null;
}
#Override
public Item findBookById(String id) {
// TODO Auto-generated method stub
return null;
}
}
To resolve annotation on parameters I create a custom RequestMappingHandlerMapping that looks all interfaces annotated with #SpringRestService
public class RestServiceRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
public HandlerMethod testCreateHandlerMethod(Object handler, Method method){
return createHandlerMethod(handler, method);
}
#Override
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
handlerMethod = new RestServiceHandlerMethod(beanName,getApplicationContext().getAutowireCapableBeanFactory(), method);
}
else {
handlerMethod = new RestServiceHandlerMethod(handler, method);
}
return handlerMethod;
}
public static class RestServiceHandlerMethod extends HandlerMethod{
private Method interfaceMethod;
public RestServiceHandlerMethod(Object bean, Method method) {
super(bean,method);
changeType();
}
public RestServiceHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean,methodName,parameterTypes);
changeType();
}
public RestServiceHandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
super(beanName,beanFactory,method);
changeType();
}
private void changeType(){
for(Class<?> clazz : getMethod().getDeclaringClass().getInterfaces()){
if(clazz.isAnnotationPresent(SpringRestService.class)){
try{
interfaceMethod = clazz.getMethod(getMethod().getName(), getMethod().getParameterTypes());
break;
}catch(NoSuchMethodException e){
}
}
}
MethodParameter[] params = super.getMethodParameters();
for(int i=0;i<params.length;i++){
params[i] = new RestServiceMethodParameter(params[i]);
}
}
private class RestServiceMethodParameter extends MethodParameter{
private volatile Annotation[] parameterAnnotations;
public RestServiceMethodParameter(MethodParameter methodParameter){
super(methodParameter);
}
#Override
public Annotation[] getParameterAnnotations() {
if (this.parameterAnnotations == null){
if(RestServiceHandlerMethod.this.interfaceMethod!=null) {
Annotation[][] annotationArray = RestServiceHandlerMethod.this.interfaceMethod.getParameterAnnotations();
if (this.getParameterIndex() >= 0 && this.getParameterIndex() < annotationArray.length) {
this.parameterAnnotations = annotationArray[this.getParameterIndex()];
}
else {
this.parameterAnnotations = new Annotation[0];
}
}else{
this.parameterAnnotations = super.getParameterAnnotations();
}
}
return this.parameterAnnotations;
}
}
}
}
I created a configuration class
#Configuration
public class WebConfig extends WebMvcConfigurationSupport{
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RestServiceRequestMappingHandlerMapping handlerMapping = new RestServiceRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager());
PathMatchConfigurer configurer = getPathMatchConfigurer();
if (configurer.isUseSuffixPatternMatch() != null) {
handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
}
if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
handlerMapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
}
if (configurer.isUseTrailingSlashMatch() != null) {
handlerMapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
}
if (configurer.getPathMatcher() != null) {
handlerMapping.setPathMatcher(configurer.getPathMatcher());
}
if (configurer.getUrlPathHelper() != null) {
handlerMapping.setUrlPathHelper(configurer.getUrlPathHelper());
}
return handlerMapping;
}
}
and I configurated it
<bean class="....WebConfig" />

Resources