使用過spring mvc的小夥伴都知道,mvc在使用的時候,咱們只須要在controller上註解上@controller跟@requestMapping(「URL」),當咱們訪問對應的路徑的時候,框架便會幫咱們去映射到指定的controller裏面的指定方法,那麼這一切都是怎麼作到的呢?還有咱們所傳遞過去的參數,爲何經過request.getParam就能輕易地 拿到呢?你們都知道mvc的核心控制器DispacherServlet的基本運行流程,那麼他的內部是怎麼運行的呢,咱們來作一下簡單的實現,讓咱們能進一步的瞭解MVC。以助於咱們從此的開發。前端
SpringMVC流程:
一、 用戶發送請求至前端控制器DispatcherServlet。java
二、 DispatcherServlet收到請求調用HandlerMapping處理器映射器。web
三、 處理器映射器找到具體的處理器(能夠根據xml配置、註解進行查找),生成處理器對象及處理器攔截器(若是有則生成)一併返回給DispatcherServlet。spring
四、 DispatcherServlet調用HandlerAdapter處理器適配器。編程
五、 HandlerAdapter通過適配調用具體的處理器(Controller,也叫後端控制器)。後端
六、 Controller執行完成返回ModelAndView。api
七、 HandlerAdapter將controller執行結果ModelAndView返回給DispatcherServlet。mvc
八、 DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器。app
九、 ViewReslover解析後返回具體View。框架
十、DispatcherServlet根據View進行渲染視圖(即將模型數據填充至視圖中)。
十一、 DispatcherServlet響應用戶。
這次實現基於Servlet 3.0,SpringBoot 2.0.1,採用註解的方式實現.這裏我先把個人demo的工程包結構先貼出來:
pom文件:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency>
首先,咱們先從註解入手,爲何在類上面標註一下@controller以及@RequestMapping,他就能其效果呢? 咱們先來定義出這兩個註解 @Controller :
@Target(ElementType.TYPE)//表示註解運行在哪裏 這裏表示只能註解再類上面 @Retention(RetentionPolicy.RUNTIME)//表示註解的(生命週期)哪來出現 public @interface WuzzController { }
@RequestMapping :
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface WuzzRequestMapping { String value(); }
@Service
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WuzzService { String value() default ""; }
@Autowired
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WuzzAutowired { String value() default ""; }
定義完註解後,那麼咱們建立一個TestController 來測試一下咱們本身定義的註解
@WuzzController @WuzzRequestMapping("/wuzz") public class TestController extends BaseController{ @WuzzAutowired private ServiceDemo serviceDemo; @WuzzRequestMapping("/index.do") public void index() { try { response.getWriter().write("index"+serviceDemo.get("wuzz")); } catch (IOException e) { e.printStackTrace(); } } @WuzzRequestMapping("/index1.do") public void index1() { try { response.getWriter().write("index1"); } catch (IOException e) { e.printStackTrace(); } } @WuzzRequestMapping("/index2.do") public void index2() { try { response.getWriter().write("index2"); } catch (IOException e) { e.printStackTrace(); } } @WuzzRequestMapping("/index3.do") public void index3() { try { response.getWriter().write("index3"); } catch (IOException e) { e.printStackTrace(); } } }
Service:
public interface ServiceDemo { String get(String name); } @WuzzService public class ServiceDemoImpl implements ServiceDemo { @Override public String get(String name) { return "service demo" + name; } }
很顯然,如今咱們雖然能在類,方法上註解上咱們本身定義的註解,可是他們如今是起不到咱們MVC框架中的效果的,在框架的內部確定是須要有一系列操做,才能使得這些註解起效果,咱們注意到,要使用MVC的時候咱們一般須要配置一個註解掃描包。而後確定是將有這些特定註解的類掃描出來,並建立出映射的路徑,才能達到咱們預期的效果。
如今咱們能夠作一個小小的測試:因此我這裏建了一個Test類來作簡單的獲取指定類(TestController)裏面有沒有咱們的註解:
public class Test { private static final Logger LOGGER = LogManager.getLogger(Test.class); public static void main(String[] args) { // Class Class clazz = TestController.class; //判斷這個類是否存在 @WuzzController if (clazz.isAnnotationPresent(WuzzController.class)) { LOGGER.info(clazz.getName() + "被標記爲controller"); String path = ""; //判斷clazz是否存在註解@WuzzRequestMapping if (clazz.isAnnotationPresent(WuzzRequestMapping.class)) { //取出註解的值 放入path WuzzRequestMapping reqAnno =
(WuzzRequestMapping)clazz.getAnnotation(WuzzRequestMapping.class); path = reqAnno.value().toString(); } Method[] ms = clazz.getMethods();//拿到控制類全部公開方法遍歷 for (Method method : ms) { //若是不存在該註解 就進入下一輪 if (!method.isAnnotationPresent(WuzzRequestMapping.class)) { continue; } LOGGER.info("方法"+method.getName()+",映射的對外路徑:" + path
+ method.getAnnotation(WuzzRequestMapping.class).value().toString()); } } } }
這裏咱們運行後的結果爲:
這樣咱們就能夠拿到指定的類裏面的指定的一些註解的值,還能夠作一系列的操做。好,那麼如今咱們須要想到的就是核心控制器DispacherServlet了。既然是servlet,咱們先來看一下servlet的生命週期。Servlet 生命週期可被定義爲從建立直到毀滅的整個過程。如下是 Servlet 遵循的過程:
- Servlet 經過調用 init () 方法進行初始化。
- Servlet 調用 service() 方法來處理客戶端的請求。
- Servlet 經過調用 destroy() 方法終止(結束)。
- 最後,Servlet 是由 JVM 的垃圾回收器進行垃圾回收的。
既然知道了servlet的生命週期,那就好辦了,咱們能夠經過servlet的初始化,將指定包下的類都掃描起來,而後再重寫service()方法去處理這些請求,不久能夠了麼?接下去咱們試一試。
建立本身的DispacherServlet:我是再spring boot環境下去操做的。咱們要配置好攔截路徑,基準包並重寫init(),service()方法
@WebServlet(urlPatterns = {"*.do"},loadOnStartup = 1,initParams = {@WebInitParam(name = "basePackage", value = "com.wuzz.demo")}) public class WuzzDispacherServlet extends HttpServlet { private static final long serialVersionUID = 1L; //保存url和Method的對應關係 private Map<String, Method> handlerMapping = new HashMap<String, Method>(); //保存掃描的全部的類名 private List<String> classNames = new ArrayList<String>(); //存放所掃描出來的類及其實例 private Map<String, Object> ioc = new HashMap<String, Object>(); public WuzzDispacherServlet() { super(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //訪問地址http://localhost:8081/wuzz/index.do //這裏拿到uri : /wuzz/index.do String uri = req.getRequestURI(); //從方法map裏獲取到映射到的方法實例 : public void com.example.demo.annotation.TestController.index() //處理成相對路徑 if (!this.handlerMapping.containsKey(uri)) { resp.getWriter().write("404 Not Found!!!"); return; } Method method = this.handlerMapping.get(uri); //經過反射拿到method所在class,拿到class以後仍是拿到class的名稱 //再調用toLowerFirstCase得到beanName String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName()); BaseController controller; try { //獲取實例 controller = (BaseController) ioc.get(beanName); //初始化該controller的請求與響應 //也就是咱們的請求中參數怎麼經過requset.getParam方法拿到的緣由 System.out.println(req.getRequestURI()); controller.init(req, resp); //而後調用該方法 method.invoke(controller); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void init(ServletConfig config) throws ServletException { //獲取基礎掃描包: 這裏設定爲com.wuzz.demo String basePackage = config.getInitParameter("basePackage"); //1 掃描包獲得全部的class 而且注入ioc doScanner(basePackage); //二、初始化掃描到的類,而且將它們放入到ICO容器之中 doInstance(); //3.實際上這裏中間能夠掃描@Service @Autowired 註解實現自動的依賴注入 //可參考DispacherServlet 的初始化流程 //可參考DispacherServlet#initStrategies(ApplicationContext context) doAutowired(); //四、初始化HandlerMapping initHandlerMapping(); } //掃描出相關的類 private void doScanner(String scanPackage) { //scanPackage = com.gupaoedu.demo ,存儲的是包路徑 //轉換爲文件路徑,實際上就是把.替換爲/就OK了 //classpath URL url = this.getClass().getClassLoader().getResource("" + scanPackage.replaceAll("\\.", "/")); File classPath = new File(url.getFile()); for (File file : classPath.listFiles()) { if (file.isDirectory()) { doScanner(scanPackage + "." + file.getName()); } else { if (!file.getName().endsWith(".class")) { continue; } String className = (scanPackage + "." + file.getName().replace(".class", "")); classNames.add(className); } } } private void doInstance() { //初始化,爲DI作準備 if (classNames.isEmpty()) { return; } try { for (String className : classNames) { Class<?> clazz = Class.forName(className); //什麼樣的類才須要初始化呢? //加了註解的類,才初始化,怎麼判斷? //爲了簡化代碼邏輯,主要體會設計思想,只舉例 @Controller和@Service, // @Componment...就一一舉例了 if (clazz.isAnnotationPresent(WuzzController.class)) { Object instance = clazz.newInstance(); //Spring默認類名首字母小寫 String beanName = toLowerFirstCase(clazz.getSimpleName()); ioc.put(beanName, instance); } else if (clazz.isAnnotationPresent(WuzzService.class)) { //一、自定義的beanName WuzzService service = clazz.getAnnotation(WuzzService.class); String beanName = service.value(); //二、默認類名首字母小寫 if ("".equals(beanName.trim())) { beanName = toLowerFirstCase(clazz.getSimpleName()); } Object instance = clazz.newInstance(); ioc.put(beanName, instance); //三、根據類型自動賦值,投機取巧的方式 for (Class<?> i : clazz.getInterfaces()) { if (ioc.containsKey(i.getName())) {//接口如有多個實現 throw new Exception("The 「" + i.getName() + "」 is exists!!"); } //把接口的類型直接當成key了 ioc.put(i.getName(), instance); } } else { continue; } } } catch (Exception e) { e.printStackTrace(); } } //若是類名自己是小寫字母,確實會出問題 //可是我要說明的是:這個方法是我本身用,private的 //傳值也是本身傳,類也都遵循了駝峯命名法 //默認傳入的值,存在首字母小寫的狀況,也不可能出現非字母的狀況 //爲了簡化程序邏輯,就不作其餘判斷了,你們瞭解就OK //其實用寫註釋的時間都可以把邏輯寫完了 private String toLowerFirstCase(String simpleName) { char[] chars = simpleName.toCharArray(); //之因此加,是由於大小寫字母的ASCII碼相差32, // 並且大寫字母的ASCII碼要小於小寫字母的ASCII碼 //在Java中,對char作算學運算,實際上就是對ASCII碼作算學運算 chars[0] += 32; return String.valueOf(chars); } //自動依賴注入 private void doAutowired() { if (ioc.isEmpty()) { return; } for (Map.Entry<String, Object> entry : ioc.entrySet()) { //Declared 全部的,特定的 字段,包括private/protected/default //正常來講,普通的OOP編程只能拿到public的屬性 Field[] fields = entry.getValue().getClass().getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(WuzzAutowired.class)) { continue; } WuzzAutowired autowired = field.getAnnotation(WuzzAutowired.class); //若是用戶沒有自定義beanName,默認就根據類型注入 //這個地方省去了對類名首字母小寫的狀況的判斷,這個做爲課後做業 //小夥伴們本身去完善 String beanName = autowired.value().trim(); if ("".equals(beanName)) { //得到接口的類型,做爲key待會拿這個key到ioc容器中去取值 beanName = field.getType().getName(); } //若是是public之外的修飾符,只要加了@Autowired註解,都要強制賦值 //反射中叫作暴力訪問, 強吻 field.setAccessible(true); try { //用反射機制,動態給字段賦值 field.set(entry.getValue(), ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } //初始化url和Method的一對一對應關係 private void initHandlerMapping() { if (ioc.isEmpty()) { return; } for (Map.Entry<String, Object> entry : ioc.entrySet()) { Class<?> clazz = entry.getValue().getClass(); if (!clazz.isAnnotationPresent(WuzzController.class)) { continue; } //保存寫在類上面的@GPRequestMapping("/demo") String baseUrl = ""; if (clazz.isAnnotationPresent(WuzzRequestMapping.class)) { WuzzRequestMapping requestMapping = clazz.getAnnotation(WuzzRequestMapping.class); baseUrl = requestMapping.value(); } //默認獲取全部的public方法 for (Method method : clazz.getMethods()) { if (!method.isAnnotationPresent(WuzzRequestMapping.class)) { continue; } WuzzRequestMapping requestMapping = method.getAnnotation(WuzzRequestMapping.class); //優化 // //demo///query String url = ("/" + baseUrl + "/" + requestMapping.value()) .replaceAll("/+", "/"); handlerMapping.put(url, method); System.out.println("Mapped :" + url + "," + method); } } } }
最後要使這個@WebServlet 起效果,須要配置啓動類:
@SpringBootApplication @ServletComponentScan public class App { private final static Logger log = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); log.info("服務啓動成功"); } }
通過以上的這些操做,咱們本身定義的註解就能生效了,那麼如今咱們須要考慮的是,這個controller裏面,我須要獲取請求參數和響應要怎麼作呢,其實,咱們只要在初始化controller的時候將requset跟response給他塞進去不久好了嘛?咱們能夠建立一個controller的基類
public class BaseController { protected HttpServletRequest request; protected HttpServletResponse response; public void init(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; } public HttpServletRequest getRequest() { return request; } public void setRequest(HttpServletRequest request) { this.request = request; } public HttpServletResponse getResponse() { return response; } public void setResponse(HttpServletResponse response) { this.response = response; } }
這樣子,咱們在service()方法內去獲取該controller實例的時候controller.init(req, resp); 給他插進去這兩個東西,就完事了。最後啓動主類,你會發現它真的就調用了controller下的方法。
若是啓用XML的形式去作的話,大體大邏輯仍是同樣的,只不過須要以下修改:
1.添加web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>Gupao Web Application</display-name> <servlet> <servlet-name>gpmvc</servlet-name> <servlet-class>com.gupaoedu.mvcframework.v2.servlet.GPDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>application.properties</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>gpmvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
2.去掉 @WebServlet 註解,添加 application.properties 內容以下:
basePackage = com.wuzz.demo
3.既然添加了配置文件,那麼咱們須要掃描配置文件獲取配置信息
//保存application.properties配置文件中的內容 private Properties contextConfig = new Properties(); //加載配置文件 private void doLoadConfig(String contextConfigLocation) { //直接從類路徑下找到Spring主配置文件所在的路徑 //而且將其讀取出來放到Properties對象中 //相對於scanPackage=com.gupaoedu.demo 從文件中保存到了內存中 InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation); try { contextConfig.load(fis); } catch (IOException e) { e.printStackTrace(); }finally { if(null != fis){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
加載配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
掃描相關的類
doScanner(contextConfig.getProperty("scanPackage"));
4.修改pom文件爲war包形式
contextConfigLocation 是咱們在web.xml中配置的。而後用Tomcat啓動便可。