Spring MVC and AOP: @Pointcuts only apply for Rest

2019-07-24 17:54发布

问题:

I am working with Spring Framework 4.3.3 in a web environment.

I have a @Controller used for Web requests through a Web Browser that uses how dependency other @Controller but for Rest purposes. It latter mentioned uses a @Service etc...

This approach about a 'Web' using a 'Rest' how a dependency is explained in Content Negotiation using Spring MVC for the Combining Data and Presentation Formats section. Until here for development/testing and production works fine. It is a valuable approach.

Note The Rest class is annotated with @Controller because I work with ResponseEntity<?> and @ResponseBody.

The problem is with AOP

About its infrastructure I have:

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {

}

About the @Controllers I have these two classes:

  • PersonaDeleteOneController with:
    • deleteOne(@PathVariable String id, Model model) for @GetMapping
    • deleteOne(@PathVariable String id, RedirectAttributes redirectAttributes) for @DeleteMapping
  • PersonaRestController
    • deleteOne(@PathVariable String id) for @DeleteMapping

These two classes are declared within the same package named:

  • com.manuel.jordan.controller.persona

I have the following @Pointcut:

@Pointcut(value=
"execution(* com.manuel.jordan.controller.*.*Controller.deleteOne(String, ..)) 
&& args(id) && target(object)")
public void deleteOnePointcut(String id, Object object){}

That @Pointcut is used for the following advice:

@Before(value="ControllerPointcut.deleteOnePointcut(id, object)")
public void beforeAdviceDeleteOne(String id, Object object){
    logger.info("beforeAdviceDeleteOne - @Controller: {} - Method: deleteOne - id: {}", object.getClass().getSimpleName(), id);
}

When I execute the Rest tests I can confirm through AOP + logging that prints the following pattern:

  • @Controller (Rest) -> @Service -> @Repository

Until here all work how is expected

When I execute the Web tests I can confirm through AOP + logging that prints the following pattern:

  • @Controller (Rest) -> @Service -> @Repository

What I need or expect is the following:

  • @Controller (Web) -> @Controller (Rest) -> @Service -> @Repository

What is wrong or missing?. The deleteOne signatures are not ambiguous about their parameters.

Same case for Production.

Alpha

Here the controllers:

@Controller
@RequestMapping(value="/personas")
public class PersonaDeleteOneController {

    private final PersonaRestController personaRestController;

    @Autowired
    public PersonaDeleteOneController(PersonaRestController personaRestController){
        this.personaRestController = personaRestController;
    }

    @GetMapping(value="/delete/{id}",
                produces=MediaType.TEXT_HTML_VALUE)
    public String deleteOne(@PathVariable String id, Model model){
        model.addAttribute(personaRestController.findOneById(id));
        model.addAttribute("root", "/personas/delete");
        return "persona/deleteOne";
    }

    @DeleteMapping(value="/delete/{id}",
                   produces=MediaType.TEXT_HTML_VALUE)
    public String deleteOne(@PathVariable String id, RedirectAttributes redirectAttributes){
        personaRestController.deleteOne(id);
        redirectAttributes.addFlashAttribute("message", "process.successful");
        return "redirect:/message";
    }

}

And

@Controller
@RequestMapping(value="/personas")
public class PersonaRestController {

    private final PersonaService personaService;

    @Autowired
    public PersonaRestController(PersonaService personaService){
        this.personaService = personaService;
    }

    @DeleteMapping(value="/{id}")
    public ResponseEntity<Void> deleteOne(@PathVariable String id){
        personaService.deleteOne(id);
        return ResponseEntity.noContent().build();
    }

    ....

How you can see I don't use this. to execute the method invocations.

回答1:

Seems that the problem is in you pointcut definition. You may notice that your advice method is performed only for methods with one parameter, so this is due to the fact that you have specified args(id) in the pointcut declaration. It must work as you expect if you remove args(id), but in this case some workaround must be used to expose parameter value.

I think that this is strange behavior form AspectJ because constructions like execution(* *.*(String, ..)) && args(arg) && target(t)) has clear semantic sense to capture all the methods with String first parameter and expose it to args. It can be a bug or feature, at least, to AspectJ developers.

To get what you want, you can use a workaround with joinPoint.getArgs() inside advice method like this:

@Pointcut(value=
"execution(* com.manuel.jordan.controller.*.*Controller.deleteOne(..)) && target(object)")
public void deleteOnePointcut(Object object){}

@Before(value="ControllerPointcut.deleteOnePointcut(object)")
public void beforeAdviceDeleteOne(JoinPoint jp, Object object){
    Object id = jp.getArgs()[0];
    logger.info("beforeAdviceDeleteOne - @Controller: {} - Method: deleteOne - id: {}", object.getClass().getSimpleName(), id);
}