Working With Filters in Spring
Check out this post to learn more about working with filters in Spring.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I am going to talk about how to best implement filters over HTTP requests in Spring. That is, assuming we have a program listening in a URI, we can specify that we want to execute something before the requests are processed by the controller.
This is very useful if we want all the requests to meet a requirement, for example, that they must include a specific header.
As usual, the best way to understand the concept is by writing an example program and explaining it. The source of the program that I have written below is on my GitHub page.
In this program, there is a controller and listening REST requests in the class PrincipalCrontoller.java
. This is the code for the class:
@RestController
public class PrincipalController {
@Autowired
SillyLog sillyLog;
@GetMapping("*")
public String entryOther(HttpServletRequest request,HttpServletResponse response)
{
sillyLog.debug("In entryOther");
if (response.getHeader("PROFE")!=null)
sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
if (response.getHeader("CAKE")!=null)
sillyLog.debug("Header contains CAKE: "+response.getHeader("CAKE"));
return "returning by function entryOther\r\n"+
sillyLog.getMessage();
}
@GetMapping(value={"/","one"})
public String entryOne(HttpServletRequest request,HttpServletResponse response)
{
sillyLog.debug("In entryOne");
if (response.getHeader("PROFE")!=null)
{
sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
return entryTwo(response);
}
return "returning by function entryOne\r\n"+
sillyLog.getMessage();
}
@GetMapping("two")
public String entryTwo(HttpServletResponse response)
{
sillyLog.debug("In entryTwo");
if (response.getHeader("PROFE")!=null)
sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
return "returning by function entryTwo\r\n"+
sillyLog.getMessage();
}
@GetMapping("three")
public String entryThree()
{
sillyLog.debug("In entryThree");
return "returning by function entryThree\n"+
sillyLog.getMessage();
}
@GetMapping("redirected")
public String entryRedirect(HttpServletRequest request)
{
sillyLog.debug("In redirected");
return "returning by function entryRedirect\n"+
sillyLog.getMessage();
}
}
The entryOrder
function will capture all GET requests, which the URIs aren't defined in the program. The entryOne
function will be processing the GET requests to http://localhost:8080/one or http://localhost:8080/.
The SillyLog
is a simple class where we add logs entries and then return them in the body response, so we will be able to see by where our request has gone.
In this application, it is defined as three filters: MyFilter.java
, OtherFilter.java
, and CakesFilter.java
. The first one has preference over the second because of the parameter passed in the label @Order
. I will speak more on the third filter later.
In the file MyFilter.java
, we define our first filter as:
@Component
@Order(1)
public class MyFilter implements Filter{
@Autowired
SillyLog sillyLog;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse myResponse= (HttpServletResponse) response;
sillyLog.debug("Filter: URL"
+ " called: "+httpRequest.getRequestURL().toString());
if (httpRequest.getRequestURL().toString().endsWith("/one")){
myResponse.addHeader("PROFE", "FILTERED");
chain.doFilter(httpRequest, myResponse);
return;
}
if (httpRequest.getRequestURL().toString().endsWith("/none")){
myResponse.setStatus(HttpStatus.BAD_GATEWAY.value());
myResponse.getOutputStream().flush();
myResponse.getOutputStream().println("-- I don't have any to tell you --");
return; // No hago nada.
}
if (httpRequest.getRequestURL().toString().endsWith("/redirect")){
myResponse.addHeader("PROFE", "REDIRECTED");
myResponse.sendRedirect("redirected");
chain.doFilter(httpRequest, myResponse);
return;
}
if (httpRequest.getRequestURL().toString().endsWith("/cancel")){
myResponse.addHeader("PROFE", "CANCEL");
myResponse.setStatus(HttpStatus.BAD_REQUEST.value());
myResponse.getOutputStream().flush();
myResponse.getOutputStream().println("-- Output by filter error --");
chain.doFilter(httpRequest, myResponse);
return;
}
chain.doFilter(request, response);
}
}
The OtherFilter
class is a bit easier:
@Component
@Order(2)
public class OtherFilter implements Filter{
@Autowired
SillyLog sillyLog;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest= (HttpServletRequest) request;
HttpServletResponse myResponse= (HttpServletResponse) response;
sillyLog.debug("OtherFilter: URL"
+ " called: "+httpRequest.getRequestURL().toString());
if (myResponse.getHeader("PROFE")!=null)
sillyLog.debug("OtherFilter: Header contains PROFE: "+myResponse.getHeader("PROFE"));
chain.doFilter(request, response);
}
}
The first thing we have to do to define a general filter as it is tagged in the class with the label @Component
. We should also implement the Filter
interface. Our class could also extend from OncePerRequestFilter
, which implements the interface Filter
and adds some features. This is so that the filter is only executed once by request. In this example, we are going to simplify it to the maximum and we will directly implement the Filter
interface.
The Filter
interface has three functions. void init(FilterConfig filterConfig)
throws ServletException
. This function will be executed by the web container, so it will be only executed once when the component is instantiated by Spring. void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException
, ServletException
. This function will be executed each time an HTTP request is received. We will be able to see the content of the HTTP request in the ServletRequest
object and we can modify the answer in the ServletResponse
object.
FilterChain
will be used to continue the flow of the request. void destroy()
is called by the Spring web container to indicate to the filter that it will stop being active.
As I mentioned earlier, the label @Order
allows us to specify the order in which each filter will be executed. In this case, the MyFilter
filter will run before the OtherFilter
filter.
MyFilter
class performs different actions depending on the URL called. OtherFilter
only adds a log when it is executed.
In the example code, we use only the doFilter
function. First, it converts the ServletResponse
class to HttpServletResponse
and then the ServletRequest
class to HttpServletRequest
. This is necessary in order to be able to access certain properties of objects that would not otherwise be available.
I'm going to explain, step by step, the different cases contemplated in the MyFilter
class, depending on the URL invoked.
/one
: We add a PROFE header with the FILTERED value to the response. It is important to emphasize that we can only modify the response if the request is unalterable. This is after we run the DoFilter function of the chain class so that the flow is continued. In this case, we will run the second filter, and then, it throws the entryOne function of the controller.
The first line is returned by the entryTwo
function. The logs added are shown below. I recommend watching the source code if you don't understand where are going out so many lines.
> curl -s http://localhost:8080/one
returning by function entryTwo
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/1 Filter: URL called: http://localhost:8080/one
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/2 OtherFilter: URL called: http://localhost:8080/one
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/3 OtherFilter: Header contains PROFE: FILTERED
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/4 In entryOne
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/5 Header contains PROFE: FILTERED
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/6 In entryTwo
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/7 Header contains PROFE: FILTERED
> curl -s http://localhost:8080/one
returning by function entryTwo
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/1 Filter: URL called: http://localhost:8080/one
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/2 OtherFilter: URL called: http://localhost:8080/one
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/3 OtherFilter: Header contains PROFE: FILTERED
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/4 In entryOne
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/5 Header contains PROFE: FILTERED
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/6 In entryTwo
SillyLog: 15eb34c2-cfac-4a27-9450-b3b07f44cb50/7 Header contains PROFE: FILTERED
This is better, isn't it? Now, in the headers, it shows our value for PROFE and we can see the order to redirect to http://localhost:8080/redirected. Note that the HTTP code returned is 302 (redirect). If we say for the curl to follow the redirection, passing it the -L
parameter, we will see what we expected.
curl -L -s http://localhost:8080/redirect
returning by function entryRedirect
SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/1 Filter: URL called: http://localhost:8080/redirected
SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/2 OtherFilter: URL called: http://localhost:8080/redirected
SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/3 In redirected
/none . I put the HTTP code to return the value BAD_GATEWAY and I write in the body "I don't have any to tell you". I don't run the
doFilter
function, therefore, the second filter will not be called, nor would it be passed to the controller.
> curl -s http://localhost:8080/none
-- I don't have any to tell you --
/cancel: I put the HTTP code to return the value
BAD_REQUEST
and I write in the body "Output by filter error" . I run thedoFilter
function therefore theOtherFilter
filter will be executed. After theentryOther
in the controller, it will be run.
curl -s http://localhost:8080/cancel
-- Output by filter error --
returning by function entryOther
SillyLog: 1cf7f7f9-1a9b-46a0-9b97-b8d5caf734bd/1 Filter: URL called: http://localhost:8080/cancel
SillyLog: 1cf7f7f9-1a9b-46a0-9b97-b8d5caf734bd/2 OtherFilter: URL called: http://localhost:8080/cancel
SillyLog: 1cf7f7f9-1a9b-46a0-9b97-b8d5caf734bd/3 OtherFilter: Header contains PROFE: CANCEL
SillyLog: 1cf7f7f9-1a9b-46a0-9b97-b8d5caf734bd/4 In entryOther
SillyLog: 1cf7f7f9-1a9b-46a0-9b97-b8d5caf734bd/5 Header contains PROFE: CANCEL
/otros
: In any another call, thedoFilter
function of thechain
class is executed inMyFilter
. After the second filter is executed, finally, some function in the controller will take control.
> curl -L -s http://localhost:8080/three
returning by function entryThree
SillyLog: a2dd979f-4779-4e34-b8f6-cae814370426/1 Filter: URL called: http://localhost:8080/three
SillyLog: a2dd979f-4779-4e34-b8f6-cae814370426/2 OtherFilter: URL called: http://localhost:8080/three
SillyLog: a2dd979f-4779-4e34-b8f6-cae814370426/3 In entryThree
To specify that a filter is only active for certain URLs, you must explicitly register it and not mark the class with the @Component
tag. In this example, in the FiltrosApplication
class, we see the function where a filter is added:
@Bean
public FilterRegistrationBean<CakesFilter> cakesFilter()
{
FilterRegistrationBean<CakesFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CakesFilter());
registrationBean.addUrlPatterns("/cakes/*");
return registrationBean;
}
The CakesFilter
is the next:
@Order(3)
public class CakesFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse myResponse= (HttpServletResponse) response;
myResponse.addHeader("CAKE", "EATEN");
chain.doFilter(request, response);
}
}
When you make a request to a URL that starts with /cakes/*
, you will see how the last filter is executed.
> curl -s http://localhost:8080/cakes
returning by function entryOther
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/1 Filter: URL called: http://localhost:8080/cakes
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/2 OtherFilter: URL called: http://localhost:8080/cakes
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/3 In entryOther
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/4 Header contains CAKE: EATEN
Note: Because of Spring's way of managing its context variables, it is not possible to inject the SillyLog
object with an @Autowired
. If we inject it, we will see how the variable has the value null.
Don't forget to visit my page with more articles in Spanish about Spring. You can follow me in @chuchip.
Published at DZone with permission of Jesus J. Puente. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments