For those of you who are not familiar with the basic principles of Servlets and Spring MVC, take a look at my other article which gives you a step-by-step guide to Spring MVC source code and a hand-drawn flow chart. Look back at this article and you’ll see.
This article focuses on the implementation of the code, and many of the points are explained in the code comments. Please read them carefully.
All code is hosted on https://github.com/FrancisQiang/spring-mvc-custom, anyone interested in to the fork.
Written MVC FrameworkServlet
As I mentioned in my previous article, FrameworkServlet is a direct parent of Spring MVC’s most important class, DispatcherServlet. Its main functions are: It provides the function of loading a corresponding Web application environment, as well as the unified GET, POST, DELETE, PUT and other methods to DispatcherServlet processing.
In short, some doXxx methods are assigned to a method of DispatcherServlet. What is this method? The famous doDispatch().
We can implement the simplest FrameworkServlet class.
// HttpServlet must be inherited
public class FrameworkServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// Use the doDispatch method to process the request
doDispatch(req, resp);
} catch(Exception e) { e.printStackTrace(); }}@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// Use the doDispatch method to process the request
doDispatch(req, resp);
} catch(Exception e) { e.printStackTrace(); }}public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{
// The main point here is that it is mainly implemented by DispatcherServlet
Of course you can use abstract methods here and define frameworkServlets as abstract classes
// The source code in Spring MVC does the same}}Copy the code
Of course, in order to inherit HttpServlet you need to package pom.xml in maven project, you can also package in other ways yourself.
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
Copy the code
That is, the FrameworkServlet simply provides a template method (for those unfamiliar with design patterns, this is a common but very simple design pattern for frameworks) that ultimately implements the doDispatch() method in the DispatcherServlet.
Handwritten MVC DispatcherServlet primitive version
First we need to inherit the FrameworkServlet class
public class DispatcherServlet extends FrameworkServlet{
@Override
public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// Process the request}}Copy the code
It’s easy to inherit and implement, but we seem to have forgotten that we need to configure our DispatcherServlet in web.xml.
<web-app>
<display-name>Archetype Created Web Application</display-name>
<! Configure our DispatcherServlet -->
<servlet>
<! -- Specify class -->
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>com.lgq.servlet.DispatcherServlet</servlet-class>
<! We need to specify our spring MVC configuration file path, which I will talk about later.
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>springmvc.xml</param-value>
</init-param>
<! -- Initialize policy -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<! -- DispatcherServlet core intercepts all requests -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Copy the code
At this point we need to create an MVC configuration file, springmVC.xml (note that the path is important), which is actually a Spring configuration file. The main reason we need it is that we need to load the processor mapping, processor adapter we wrote later into the IOC container. And we initialize the container to get the bean when the Servlet is initialized.
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<! -- Quite simply, nothing has been implemented yet -->
</beans>
Copy the code
To use Spring’s IOC functionality we need to import the corresponding package in Maven’s POM.xml. Import core and Context packages
Remember: MVC functionality is built on IOC, so IOC is a core feature in Spring.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.0. RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0. RELEASE</version>
</dependency>
Copy the code
HandlerMapping for handwritten MVC
To give you a better understanding, I first posted pictures from my last blog post.
That is to say, when the user sends a request, we need to traverse the collection of HandlerMapping in doDispatch() in DispatcherServlet, and then get the processor that can handle the request from the single map and finally call the processing method to return it. Leaving the Handler aside, all we need to do now is create a HandlerMapping class (which must contain a method to get the Handler and a mapping relationship) and define a HandlerMapping collection in the DispatcherServlet.
public interface HandlerMapping {
// Get the handler
// Some mappings are not a field, such as SimpleHandlerMapping
Object getHandler(HttpServletRequest request) throws Exception;
}
Copy the code
Modify the DispatcherServlet code
public class DispatcherServlet extends FrameworkServlet{
// Define a HandlerMapping collection
private List<HandlerMapping> handlerMappings = null;
@Override
public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// Process the request}}Copy the code
Initialization of handwritten MVC
Those of you who didn’t read my last article will have questions. Where did this HandlerMapping collection come from?
The answer is in servlets. We know that servlets have initialization functions, and its subclass GenericServlet implements an init() method with no arguments of its own. We just need to implement this initialization method, Then we can either request it or initialize the Web container (depending on your load-on-startup configuration parameter in web.xml).
Let’s initialize the HandlerMapping collection.
public class DispatcherServlet extends FrameworkServlet{
private List<HandlerMapping> handlerMappings = null;
// Initialize the IOC container
private void initBeanFactory(ServletConfig config) {
// Get the contextConfigLocation configured in web. XML that is the path of the spring-mvc file
String contextLocation = config.getInitParameter("contextConfigLocation");
// Initialize the container
BeanFactory.initBeanFactory(contextLocation);
}
// Initialize handlerMappings
private void initHandlerMappings(a) {
// To get a collection of instances by type you can see the source code for BeanFactory below
handlerMappings = BeanFactory.getBeansOfType(HandlerMapping.class);
}
// Initialize the factory handlerMapping when initializing the servlet
@Override
public void init(ServletConfig config) throws ServletException {
initBeanFactory(config);
initHandlerMappings();
}
private Object getHandler(HttpServletRequest request) throws Exception {
if(handlerMappings ! =null && handlerMappings.size() > 0) {
// Iterate through the processor map collection and get the corresponding processor
for (HandlerMapping handlerMapping: handlerMappings) {
Object handler = handlerMapping.getHandler(request);
if(handler ! =null) {
returnhandler; }}}return null;
}
@Override
public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// First we iterate through the handlerMapping collection and get the corresponding handler
// Get the handler
Object handler = getHandler(request);
// Handle requests...
// If there is no adapter as shown above, we can use it directly
// handler.handlerequest () handles requests directly}}Copy the code
We found that BEFORE initializing the HandlerMapping set, I also initialized the IOC container. The purpose is to hand over the pre-defined processor mapping and other components to the IOC container for management, and then directly fetch them when needed. Here I directly give all the source code in The BeanFactory. I did this by relying on Spring’s own IOC container.
public class BeanFactory {
// This is the important context class in Spring that contains the Bean factory
private static ClassPathXmlApplicationContext context;
// Use the configuration file path to initialize the IOC container
public static void initBeanFactory(String contextConfigLocation) {
context = new ClassPathXmlApplicationContext(contextConfigLocation);
}
// Get a collection of beans by type
public static <T> List<T> getBeansOfType(Class
clazz) {
ArrayList<T> result = newArrayList<>(); Map<String, ? > beansOfType = context.getBeansOfType(clazz);for (Object object : beansOfType.values()) {
result.add((T) object);
}
return result;
}
// Get the beanName collection by type
public static List<String> getBeanNamesOfType(Class<Object> objectClass) {
String[] beanNamesForType = context.getBeanNamesForType(objectClass);
if (beanNamesForType == null) {
return null;
} else {
return newArrayList<>(Arrays.asList(beanNamesForType)); }}// Get the bean type from beanName
public staticClass<? > getType(String beanName) {return context.getType(beanName);
}
// Get the instance bean by type
public static Object getBean(Class
clazz) {
returncontext.getBean(clazz); }}Copy the code
In addition to our handlerMappings, we also need to initialize a collection of HandlerAdapters. In the above diagram, we are processing requests directly through handler. But in MVC you need to go through the middle layer of HandlerAdapter, for the reason I mentioned in the last article, and you can go back to it, it’s all about implementation.
HandlerAdapter for handwritten MVC
First of all we need is clear this is an adapter, if studied design patterns, students can learn about the adapter pattern needs through inheritance or hold on an object to achieve adaptation, and also is a kind of adapter in Spring MVC best practice, I like it also implements a code, you can have a look.
public interface HandlerAdapter {
// Processing requests returns view information
// You can see that a handler is passed in here, which is an implementation of the adapter pattern
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception;
// Determine whether the adapter supports the processor
boolean support(Object handler);
}
Copy the code
In this case, the ModelAndView is essentially the view model that will be returned after the final processing, which should be familiar to anyone who has played with Spring MVC. This is how I define ModelAndView.
public class ModelAndView {
// The model inside is a map
private Map<String,Object> model = new HashMap<>();
public Map<String, Object> getModel(a) {
return model;
}
// Add attributes
public void addAttribute(String attributeName, String attributeValue) { model.put(attributeName, attributeValue); }}Copy the code
Now we can get our heads together
This idea is very important!!
- First we will initialize our IOC container and get the HandlerMapping and HandlerAdapter collection when the Servlet is initialized.
- When a user sends a request to our doDispatch method, we first iterate through the HandlerMapping collection to get the handler that can handle the request.
- We then iterate through the collection of HandlerAdapters again to get the processor adapters that fit that processor.
- We then call the adapter’s handleRequest and return the corresponding ModelAndView view information.
Write MVC complete initialization process and complete processing process
Once we have implemented the HandlerAdapter processor and sorted out the above flow we can implement the complete DispatcherServlet.
public class DispatcherServlet extends FrameworkServlet{
// Two important components
private List<HandlerMapping> handlerMappings = null;
private List<HandlerAdapter> handlerAdapters = null;
// Initialize the IOC container
private void initBeanFactory(ServletConfig config) {
// Get the contextConfigLocation configured in web. XML that is the path of the spring-mvc file
String contextLocation = config.getInitParameter("contextConfigLocation");
// Initialize the container
BeanFactory.initBeanFactory(contextLocation);
}
// Initialize handlerMappings
private void initHandlerMappings(a) {
// To get a collection of instances by type you can see the source code for BeanFactory below
handlerMappings = BeanFactory.getBeansOfType(HandlerMapping.class);
}
// Initialize handlerAdapters
private void initHandlerAdapters(a) {
handlerAdapters = BeanFactory.getBeansOfType(HandlerAdapter.class);
}
// Initialize handlerMapping for the factory when initializing the servlet
// And HandlerAdapter initialization
@Override
public void init(ServletConfig config) throws ServletException {
initBeanFactory(config);
initHandlerMappings();
initHandlerAdapters();
}
// Get the handler
private Object getHandler(HttpServletRequest request) throws Exception {
if(handlerMappings ! =null && handlerMappings.size() > 0) {
// Iterate through the processor map collection and get the corresponding processor
for (HandlerMapping handlerMapping: handlerMappings) {
Object handler = handlerMapping.getHandler(request);
if(handler ! =null) {
returnhandler; }}}return null;
}
// Get the corresponding processor adapter from the processor
private HandlerAdapter getHandlerAdapter(Object handler) {
if(handlerAdapters ! =null && handlerAdapters.size() > 0) {
// Walk through the processor adapter collection to get the adapter that can fit the processor
for (HandlerAdapter handlerAdapter: handlerAdapters) {
if (handlerAdapter.support(handler)) {
returnhandlerAdapter; }}}return null;
}
@Override
public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// First we iterate through the handlerMapping collection and get the corresponding handler
// Get the handler
Object handler = getHandler(request);
if(handler ! =null) {
// Get the processor adapter corresponding to the processor
HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
if(handlerAdapter ! =null) {
// Calling the adapter's handler returns the ModelAndView object
ModelAndView modelAndView = handlerAdapter.handleRequest(request, response, handler);
// Here is a simple implementation of the model in the ModelAndView object
// Returns the page as a string
if(modelAndView ! =null) {
Map<String, Object> model = modelAndView.getModel();
PrintWriter writer = response.getWriter();
for (String string: model.keySet()) {
writer.write(string);
}
writer.close();
}
}
}
}
}
Copy the code
We can look at the class structure of the DispatcherServlet to help understand
Handwritten MVC implementation of a simple process
Wrote here, we will find that we so far have not specific HandlerMapping Handler, HandlerAdapter implementation class, so need to handle the request, we have to construct some simple class to be realized.
SimpleHandler
Let’s start by implementing a SimpleHandler.
public interface SimpleHandler {
// Very simple to handle requests
void handleRequest(HttpServletRequest request, HttpServletResponse response)throws Exception;
}
Copy the code
We also need an implementation class, which is very simple, and you can see it.
public class AddBookHandler implements SimpleHandler{
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception{
response.setContentType("text/html; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("Add book successful!"); writer.close(); }}public class DeleteBookHandler implements SimpleHandler{
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("text/html; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("Delete book successfully!"); writer.close(); }}Copy the code
The above two processors simply return two different messages such as add book success and delete book success. And we know that because of the single responsibility principle, our processor is only for processing requests, and we need a scheduling class, which is HandlerMapping.
SimpleHandlerMapping
public class SimpleHandlerMapping implements HandlerMapping {
@Override
public Object getHandler(HttpServletRequest request) throws Exception {
// Get the request URI
String uri = request.getRequestURI();
// Get the corresponding handler according to the mapping rule
if ("/addBook".equals(uri)) {
return new AddBookHandler();
} else if ("/deleteBook".equals(uri)) {
return new DeleteBookHandler();
}
return null; }}Copy the code
As its name implies, this is a very simple processor mapping, not even a map field, and the mapping is handled using an if else statement.
Of course, with processor mapping, you also need a processor adapter.
SimpleHandlerAdapter
public class SimpleHandlerAdapter implements HandlerAdapter {
// Process the request and return the result, null because the request is being processed by the handler
// just write some strings
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Force to SimpleHandler and call the handler's processing method
((SimpleHandler)handler).handleRequest(request, response);
return null;
}
// Check if it is SimpleHandler and return
@Override
public boolean support(Object handler) {
return handler instanceofSimpleHandler; }}Copy the code
This completes the simple processing flow, but we are missing a few steps to configure the corresponding processor, adapter, and processor mapping in the SpringMVC.xml configuration file so that these classes can be handed over to the IOC container for management. And automatic assembly during our DispatcherServlet initialization.
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="simpleHandlerMapping" name="simpleHandlerMapping" class="com.lgq.handler.SimpleHandlerMapping"/>
<bean id="simpleHandlerAdapter" name="simpleHandlerAdapter"
class="com.lgq.adapter.SimpleHandlerAdapter"/>
</beans>
Copy the code
At this point, we can configure Tomcat for the experiment. Here I use the Maven plugin for Tomcat. Just configure the plugin in Maven’s POM.xml and modify the startup parameters.
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
</configuration>
</plugin>
Copy the code
So we can have fun accessing the test.
Handwritten MVC annotation form processing flow
@Controller and @RequestMapping are annotations that we use in everyday application development. How does it work? I’m going to generalize a little bit here, but you can read my last post for details.
When the Servlet is initialized, the @Controller annotated class is first loaded into the IOC container, and then the class is evaluated for the @requestMapping annotation. If it does, it will wrap the method into a HandlerMethod. Note that this HandlerMethod is also a handler, and that the method in this HandlerMethod will hold the annotation annotation, which will eventually be called by reflection.
Now that you know the process, let’s start writing one by hand!
Custom annotations
First we’ll define @Controller and @RequestMapping annotations ourselves. If you’re not familiar with custom annotations, go back and review them.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
String value(a) default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value(a) default "";
}
Copy the code
HandlerMethod
Holding the bean instance in the HandlerMethod, the bean type, and the method referenced by @RequestMapping are the three elements that reflection must have. Here I use Lombok annotations directly to simplify getter setter methods.
@Data
@AllArgsConstructor
public class HandlerMethod {
private Object bean;
privateClass<? > beanType;private Method method;
}
Copy the code
<! -- Lombok configuration -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
Copy the code
RequestMappingHanlderMapping
This is a very important class because it involves retrieving the corresponding Controller annotated class from the IOC container
public class RequestMappingHandlerMapping implements HandlerMapping {
// store the @requestMapping URL and
// The enclosing HandlerMethod instance of the method annotated by @requestMapping
private Map<String, HandlerMethod> urlMap = new HashMap<>();
// This initialization method is important and is required in the springmVC.xml configuration file
// Configure the init-method parameter
public void init(a) {
// First get the beanNames of all objects from the IOC container
List<String> beanNames = BeanFactory.getBeanNamesOfType(Object.class);
// Get the corresponding clazz from beanName
if(beanNames ! =null) {
for(String beanName: beanNames) { Class<? > clazz = BeanFactory.getType(beanName);// Determine whether the clazz object is a processor, and if so, process it
if(clazz ! =null && isHandler(clazz)) {
// Get all methods of the class
Method[] methods = clazz.getDeclaredMethods();
// Iterate through all the methods and determine if there is a method annotating RequestMapping
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
// Get the annotation of the method and the annotation name value followed by the map key
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String value = requestMapping.value();
// Create handlerMethod by beanName, clazz, method
HandlerMethod handlerMethod = new HandlerMethod(beanName, clazz, method);
// Store the map
urlMap.put(value, handlerMethod);
}
}
}
}
}
}
// Check whether the annotation on the class exists in Controller or RequestMapping
private boolean isHandler(Class
clazz) {
return clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(RequestMapping.class);
}
@Override
public Object getHandler(HttpServletRequest request) throws Exception {
// Find the corresponding handler in the urlMap generated by init in the initialization method call
returnurlMap.get(request.getRequestURI()); }}Copy the code
There also needs to be configured for springmvc. XML configuration files, here speak RequestMappingHandlerAdapter this class first, later the configuration file is given together.
RequestMappingHandlerAdapter
public class RequestMappingHandlerAdapter implements HandlerAdapter{
// Process the request and return ModelAndView
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ModelAndView modelAndView = null;
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
if(method ! =null) {
// Call the method through reflection and encapsulate the return result into a ModelAndView object
modelAndView = (ModelAndView)method.invoke(BeanFactory.getBean(((HandlerMethod) handler).getBeanType()));
}
return modelAndView;
}
// Is it a HandlerMethod
@Override
public boolean support(Object handler) {
return handler instanceofHandlerMethod; }}Copy the code
The springmVC.xml configuration file
We also need to configure these classes once they are written, because loading them into IOC is a very, very important thing.
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="simpleHandlerMapping" name="simpleHandlerMapping" class="com.lgq.handler.SimpleHandlerMapping"/>
<! - the configuration RequestMappingHandlerMapping -- -- >
<! Init-method -->
<! You also need to note that lazy loading is used here, because during initialization we -->
<! We need to use the IOC container, so we need to wait until the IOC initialization is complete before we initialize the bean.
<bean id="requestMappingHandlerMapping" name="requestMappingHandlerMapping"
class="com.lgq.handler.RequestMappingHandlerMapping" lazy-init="true" init-method="init"/>
<! - the configuration RequestMappingHandlerAdapter -- -- >
<bean id="requestMappingHandlerAdapter" name="RequestMappingHandlerAdapter"
class="com.lgq.adapter.RequestMappingHandlerAdapter"/>
<bean id="simpleHandlerAdapter" name="simpleHandlerAdapter"
class="com.lgq.adapter.SimpleHandlerAdapter"/>
<! -- Let IOC scan the test Controller we need to write later -->
<context:component-scan base-package="com.lgq.handler"/>
</beans>
Copy the code
Write a test Controller and experiment
// This is an annotation of Spring itself
// The main thing here is to load the class into the container
// Because the annotations we write will be the same as spring itself
// @Controller conflicts, so @Component is used
// corresponds to the configuration file above
// <context:component-scan base-package="com.lgq.handler"/>
@Component
// This is our own annotation
@Controller
public class HelloWorldHandler {
// This is our annotation
@RequestMapping(value = "/helloWorld")
public ModelAndView helloWorld(a) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addAttribute("hello!!!"."World!!!");
returnmodelAndView; }}Copy the code
Let’s visit and test it
Success!!
Great, we implemented a simple Spring MVC ourselves.
This is a catalog of our entire project
conclusion
In this project, we didn’t implement the message converter, but we actually did it stealthily when we wrote again (because the response was sent to the browser before we returned the ModelAndView). HttpMessageConvert class HttpMessageConvert class HttpMessageConvert class
In fact, the whole basic flow of Spring MVC is not complicated. It is nothing more than a distributor to handle requests. There are some separation of responsibilities, such as Handler, HandlerAdapter, HandlerMapping, etc. I hope you have a clear idea of the overall flow of Spring MVC by the end of this article. Here I will put up the whole flow chart for your understanding.
This project all code is hosted on https://github.com/FrancisQiang/spring-mvc-custom, anyone interested in to the fork.
Thanks for reading!