1. 程式人生 > >2 手寫實現SpringMVC,第二節:自定義註解及反射賦值

2 手寫實現SpringMVC,第二節:自定義註解及反射賦值

還是回到最終要實現的效果。

可以發現,這裡面使用了大量的自定義註解,並且還有autuwire的屬性也需要被賦值(Spring的IOC功能)。

先來建立自定義註解


注意,根據不同的註解使用的範圍來定義@Target,譬如Controller,Service能註解到類,RequestMapping能註解到類和方法,AutoWired只能註解到屬性。

Autowired

/**
 * Created by wuwf on 17/6/27.
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    String value() default "";
}

Controller
import java.lang.annotation.*;

/**
 * Created by wuwf on 17/6/27.
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {

    String value() default "";
}
RequestMapping
import java.lang.annotation.*;

/**
 * Created by wuwf on 17/6/27.
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}
RequestParam
import java.lang.annotation.*;

/**
 * Created by wuwf on 17/6/27.
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";

    boolean required() default true;
}
Service
import java.lang.annotation.*;

/**
 * Created by admin on 17/6/27.
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
    String value() default "";
}

建立基本功能類


新增2個最簡單的Service和實現類,模擬查詢和修改。
ModifyService

public interface ModifyService {

    String add(String name, String addr);
    String remove(Integer id);
}
QueryService
public interface QueryService {
    String search(String name);
}
ModifyServiceImpl
import com.tianyalei.mvc.annotation.Service;
import com.tianyalei.mvc.service.ModifyService;

/**
 * Created by wuwf on 17/6/27.
 */
@Service
public class ModifyServiceImpl implements ModifyService {
    @Override
    public String add(String name, String addr) {
        return "invoke add name = " + name + " addr = " + addr;
    }

    @Override
    public String remove(Integer id) {
        return "remove id = " + id;
    }
}

QueryServiceImpl
package com.tianyalei.mvc.service.impl;

import com.tianyalei.mvc.annotation.Service;
import com.tianyalei.mvc.service.QueryService;

/**
 * Created by admin on 17/6/27.
 */
@Service("myQueryService")
public class QueryServiceImpl implements QueryService {
    @Override
    public String search(String name) {
        return "invoke search name = " + name;
    }
}


WebController

package com.tianyalei.mvc.controller;

import com.tianyalei.mvc.annotation.Autowired;
import com.tianyalei.mvc.annotation.Controller;
import com.tianyalei.mvc.annotation.RequestMapping;
import com.tianyalei.mvc.annotation.RequestParam;
import com.tianyalei.mvc.service.ModifyService;
import com.tianyalei.mvc.service.QueryService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by wuwf on 17/6/28.
 */
@Controller
@RequestMapping("/web")
public class WebController {
    @Autowired("myQueryService")
    private QueryService queryService;
    @Autowired
    private ModifyService modifyService;

    @RequestMapping("/search")
    public void search(@RequestParam("name") String name, HttpServletRequest request, HttpServletResponse response) {
        String result = queryService.search(name);
        out(response, result);
    }

    @RequestMapping("/add")
    public void add(@RequestParam("name") String name,
                    @RequestParam("addr") String addr,
                    HttpServletRequest request, HttpServletResponse response) {
        String result = modifyService.add(name, addr);
        out(response, result);
    }

    @RequestMapping("/remove")
    public void remove(@RequestParam("name") Integer id,
                       HttpServletRequest request, HttpServletResponse response) {
        String result = modifyService.remove(id);
        out(response, result);
    }

    private void out(HttpServletResponse response, String str) {
        try {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


至此,專案的基本結構就是這樣了。下面需要做的就是正文了,如何讓上面的註解生效,如何讓請求根據地址進到對應的方法。

掃描專案類,並例項化引數

這一步的目的是得到一個類名和類例項的對映物件,如 "webController"->WebController的例項,"searchService"->SearchServiceImpl的例項,類似於Spring的BeanFactory,將對應的例項賦給對應的beanName。譬如,searchService在Controller中被定義了,它並不需要去做new這個物件的處理,而是由我們主動注入進去。前提就是我們需要儲存一個beanName->bean物件的對映關係。這種處理就是ioc,也就是早期在spring配置檔案application.xml經常配的bean id="XXX" class="XXX". 下面來看怎麼例項化引數。 思路就是掃描目錄下所有需要被我們的山寨Spring託管的類,並將其例項化,然後儲存對映關係。在Spring裡就是所有標註了@Component註解的類,需要被託管。我們這裡就只處理@Controller和@Service註解的類,然後例項化。

掃描所有需要被對映的類

修改一下web.xml,指定我們需要掃描的包。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <servlet>
        <servlet-name>dispatchServlet</servlet-name>
        <servlet-class>com.tianyalei.mvc.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>scanPackage</param-name>
            <param-value>com.tianyalei.mvc</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatchServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>
這裡添加了一個init-param,屬性名為scanPackage(可任意起名),value為包名。
然後在DispatcherServlet的init方法中,接收並處理。
package com.tianyalei.mvc;

import com.tianyalei.mvc.annotation.Controller;
import com.tianyalei.mvc.annotation.Service;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by wuwf on 17/6/28.
 * 入口Sevlet
 */
public class DispatcherServlet extends HttpServlet {
    private List<String> classNames = new ArrayList<>();

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("我是初始化方法");
        scanPackage(config.getInitParameter("scanPackage"));
        System.out.println(classNames);
    }

    /**
     * 掃描包下的所有類
     */
    private void scanPackage(String pkgName) {
        //獲取指定的包的實際路徑url,將com.tianyalei.mvc變成目錄結構com/tianyalei/mvc
        URL url = getClass().getClassLoader().getResource("/" + pkgName.replaceAll("\\.", "/"));
        //轉化成file物件
        File dir = new File(url.getFile());
        //遞迴查詢所有的class檔案
        for (File file : dir.listFiles()) {
            //如果是目錄,就遞迴目錄的下一層,如com.tianyalei.mvc.controller
            if (file.isDirectory()) {
                scanPackage(pkgName + "." + file.getName());
            } else {
                //如果是class檔案,並且是需要被spring託管的
                if (!file.getName().endsWith(".class")) {
                    continue;
                }
                //舉例,className = com.tianyalei.mvc.controller.WebController
                String className = pkgName + "." + file.getName().replace(".class", "");
                //判斷是否被Controller或者Service註解了,如果沒註解,那麼我們就不管它,譬如annotation包和DispatcherServlet類我們就不處理
                try {
                    Class<?> clazz = Class.forName(className);
                    if (clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(Service.class)) {
                        classNames.add(className);
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        out(resp, "請求到我啦");
    }

    private void out(HttpServletResponse response, String str) {
        try {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

這裡定義了一個scanPackage的方法,是用於遞迴處理web.xml定義的需要被掃描的包下的所有等待被接管的類。我們將所有需要被託管的類,儲存到一個list中。供下一步例項化時使用。 寫完後,執行看一下被掃描的託管類:
這樣就取到了所有被Controller和Service標註的類了。

例項化bean

我們取到了所有被託管的類,下一步就是要例項化這些類了,也就是像Spring的ioc一樣,按規則給bean注入例項值。 像如果在任何地方定義webController,那麼我們就預設給他賦值為WebController的例項,定義了modifyService,那麼就預設給它注入ModifyServiceImpl的例項。倘若使用者自定義了beanName,那麼就給beanName注入值,如果不定義,就走上面預設的。如果在ModifyServiceImpl上寫了@Service("abc"),那麼我們就儲存一個"abc"->ModifyServiceImpl的對映,如果沒有定義,就儲存一個"modifyService"->ModifyServiceImpl的對映。 儲存對映的目的是在將來給AutoWired註解的屬性注入值時,好根據beanName來判斷該注入什麼例項。 新增一個map儲存beanName和例項的對映。
private Map<String, Object> instanceMapping = new HashMap<>();
新增doInstance
@Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("我是初始化方法");
        scanPackage(config.getInitParameter("scanPackage"));

        doInstance();

        System.out.println(instanceMapping);
    }

 /**
     * 例項化
     */
    private void doInstance() {
        if (classNames.size() == 0) {
            return;
        }
        //遍歷所有的被託管的類,並且例項化
        for (String className : classNames) {
            try {
                Class<?> clazz = Class.forName(className);
                //如果是Controller
                if (clazz.isAnnotationPresent(Controller.class)) {
                    //舉例:webController -> new WebController
                    instanceMapping.put(lowerFirstChar(clazz.getSimpleName()), clazz.newInstance());
                } else if (clazz.isAnnotationPresent(Service.class)) {
                    //獲取註解上的值
                    Service service = clazz.getAnnotation(Service.class);
                    //舉例:QueryServiceImpl上的@Service("myQueryService")
                    String value = service.value();
                    //如果有值,就以該值為key
                    if (!"".equals(value.trim())) {
                        instanceMapping.put(value.trim(), clazz.newInstance());
                    } else {//沒值時就用介面的名字首字母小寫
                        //獲取它的介面
                        Class[] inters = clazz.getInterfaces();
                        //此處簡單處理了,假定ServiceImpl只實現了一個介面
                        for (Class c : inters) {
                            //舉例 modifyService->new ModifyServiceImpl()
                            instanceMapping.put(lowerFirstChar(c.getSimpleName()), clazz.newInstance());
                            break;
                        }
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

private String lowerFirstChar(String className) {
        char[] chars = className.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

重新啟動執行,來看看對映的結果:

可以看到,已經按照我們的規則儲存好了beanName和例項之間的對映關係了。

下一步就是處理AutoWired了,真正的ioc賦值。請看下一篇。