1. 程式人生 > >手動實現一個簡易版SpringMvc

手動實現一個簡易版SpringMvc

版權宣告:本篇部落格大部分程式碼引用於公眾號:java團長,我只是在作者基礎上稍微修改一些內容,內容僅供學習與參考

前言:目前mvc框架經過大浪淘沙,由最初的struts1到struts2,到目前的主流框架SpringMvc,並逐漸區域佔領市場主流穩定狀態,由於其背後強大的Spring家族提供了一系列高可用的元件和服務,SpringMvc在短時間內肯定是無法被超越的。公司裡開發的專案第一首先框架就是SpringMvc,在市場上如火如荼,甚囂塵上。今天我們就來自己手動實現一個簡歷的閹割版SpringMvc,主要的目的就是體會SpringMvc的設計思路和理念,瞭解其幕後工作流程,當然需要注意的是本次實現的是簡易版,程式碼不超過500行,所以本篇部落格姑且只可領會設計思路,拋磚引玉,不可陷入細枝末節、吹毛求疵的死衚衕裡

本篇部落格的目錄

一:springMvc的使用方法

二:專案開發結構

三:簡易版springmvc的實現

四:總結

一:springMvc的使用方法

   1.1:開發基本過程

     springmvc開發過程是典型的mvc模式,首先在xml裡配置dispatcherServlet(假如沒有用到springboot的話),然後再配置掃描包,註解驅動配置,在程式碼層面又用Controller上註解@RequestMapping對映請求,請求進入後,方法中用各種註解比如@pathvarible獲得url請求引數,再到業務方法中的呼叫service,service再呼叫dao層,實現資料庫的增刪改查的業務性操作,之後再返回檢視或者用@responseBoby修飾的返回資料,這已經成為一種典型的呼叫和開發模式!

1.2:基本原理

  首先是xml配置,springmvc需要讀取xml裡面的配置,然後放在簡單的一個集合資料模型裡(Map),再然後就是裝載使用者的配置,掃描基準包,獲取使用者新增的註解,獲取註解資訊和配置的引數,再用spring的IOC初始化配置的Bean例項,自動注入類例項(比如contoller的依賴service、dao層mappeer等,等容器啟動起來,如果有請求進來,就會按照路徑對映對應的controller,再選擇方法進行處理完成一系列操作後返回給客戶端。

二:專案開發結構

2.1:專案基本結構

這是一個簡易版的springmvc,其中包含了基本的使用註解自己實現,和後臺的預設控制器DispatcherServlet,還有解析xml的工具,和預設xml配置資源,我們使用的serlvet是3.1版本,採用了對基本servlet進行了二次封裝,以下是基本的程式碼:

 

2.2:具體的開發過程示例

@MyRequestMapping(name = "/orderApi")
@MyController(name = "order")
public class OrderController {

    @MyQualifer(name = "orderServiceImpl")
    private OrderServiceImpl orderServiceImPl;

    @MyRequestMapping(name = "/create")
    public void create() {
        orderServiceImPl.createOrder();
    }
}
@MyRepository(name = "orderDao")
public class OrderDao {

public void insert() {
System.out.println("資料庫中插入一條記錄");
}
}
public interface OrderServiceEx {

public void createOrder();
}
@MyService(name = "orderServiceImpl")
public class OrderServiceImpl implements OrderServiceEx {

@MyQualifer(name = "orderDao")
private OrderDao orderDao;

public void createOrder() {

}
}

 

三:簡易版springmvc的實現

3.1:讀取xml配置

 解析xml採用的是dom4j的1.6.1版本,目的是模仿springmvc解析使用者的配置檔案,讀取使用者配置的掃描包,以下是xmlUtil的原始碼:

public class XmlUtil {

    public static String getNodeValue(String nodeName, String xmlPath) {
        try {
            // 將字串轉為xml
            Document document = DocumentHelper.parseText(xmlPath);
            // 查詢節點
            Element element = (Element) document.selectSingleNode(nodeName);
            if (element != null) {
                return element.getStringValue();
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return "";
    }
}

 

3.2:定義註解

3.2.1:模擬Reposity註解

作用就是告訴spring容器將依賴例項化注入到需要的呼叫類裡面,其中用了name這個屬性,就是讓spring根據名字去尋找對應的bean進行依賴注入。並標識其執行在類上面(無法放在方法上)

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRepository {
    public String name();
}

3.2.2:模擬controller註解

作用就是告訴spring容器這是一個控制器,並交給spring去例項化,可以執行在類和方法上

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
    public String name();
}

3.2.3:模擬Quanlifer註解

作用於成員變數上,主要的作用是讓spring根據配置的bean的名字進行注入:

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyQualifer {
    public String name();
}

3.2.4:模擬@service註解

作用於類或者介面上,主要的作用就是標識一個類為service層,並交給spring去初始化!

@Documented
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService {
    public String name();
}

 3.2.5:MyDispatcherServlet

 模擬DispatcherServlet,主要的作用就是作為請求控制器,處理客戶端請求,繼承自HttpServlet,我使用的版本是3.1.0,因此可以用註解來取得一些引數:

@WebServlet(name = "dispatherServlet", urlPatterns = "/", loadOnStartup = 1)
public class MyDispatcherServlet extends HttpServlet {

    /**
     * 掃描的基準包
     */
    private String basePackage = "";
    /**
     * 包名
     */
    private List<String> packageNames = new ArrayList<>();
    /**
     * 註解名和類例項
     */
    private Map<String, Object> instanceMap = new HashMap<String, Object>();
    /**
     * 包名和註解名
     */
    private Map<String, String> nameMap = new HashMap<>();
    /**
     * url和方法
     */
    private Map<String, Method> urlMethodMap = new HashMap<>();
    /**
     * Method和包名
     */
    private Map<Method, String> methodPackageMap = new HashMap<>();
    @Override
    public void init(ServletConfig config) {
        try {
            getBasePackageFromConfigXml();
            scanBasePackage(basePackage);
            instance(packageNames);
            springIoc();
            handleUrlMethodMap();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

這裡是一個流程處理類,第一步從xml裡面獲取使用者配置的資訊,這裡的主要作用就是掃描出配置的包名,第二步就是掃描基準包的路徑然後放在一個List中,儲存起來。第三步:根據掃描出來的包,獲取包裡面的類class,然後利用反射獲取類上的class資訊(不包含方法),然後把註解名和對應的類例項儲存在map中,再把包名和註解的引數(這裡主要是controller、service等在類上的)儲存起來。第四步:模擬spring的IOC注入例項的過程,遍歷第三步中的註解名和類例項map,然後利用反射例項化欄位!第五步:遍歷整個包下的類,然後獲取類中的所有方法,再取得MyRequestMapping的註解上的配置的引數,把它存在urlMethodMap中,這裡的作用就是把類上的MyRequestingMapping的配置的引數和方法上的註解配置的引數組裝起來!最終在dopost方法中處理:

3.2.6:getBasePackFromConfig方法

 整個方法的目的就是解析xml,獲取基準包,這是我們繼續往下的基礎,因為只有獲取包路徑,我們才能掃描到註解,才能知道自己要開發的類在哪個目錄

    /**
     * 從xml中獲取basePackage
     * @return
     */
    private void getBasePackageFromConfigXml() {
        final String basePackageName = XmlUtil.getNodeValue("scan-package-path", "springmvcConfig.xml");
        basePackage=basePackageName;
   }

3.2.7:scanBasePackage

掃描基礎包:這是一個遞迴的方法,主要是解析路徑,獲取目錄下的檔案,然後新增檔名到packageNames這個list,儲存了我們掃描的路徑下的所有檔案,接下來就是迴圈遍歷這個list,取出其中的class再進一步的處理

    /**
     * 掃描基礎包,儲存類路徑到list中
     * @param basePackage
     */
    private void scanBasePackage(String basePackage) {
        final URL url = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));
        final File basePackageFile = new File(url.getPath());
        System.out.println("掃描到的檔案是:" + basePackageFile.getName());
        final File[] files = basePackageFile.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                scanBasePackage(basePackage + "." + file.getName());
            } else if (file.isFile()) {
                packageNames.add(basePackage + "." + file.getName().split("\\.")[0]);
            }
        }
    }

3.2.8:instance(packageNames)

該方法的主要目的就是遍歷包下面的類,掃描類上面的註解,主要是@MyController、@MyService、@MyRepository,然後利用反射獲取類上的註解,取得註解上配置的引數(就是name上的值,在下面的例項中,比如:instanceMap將會存放("order",OrderController.class),nameMap將會存放("test.java.com.wyq","order")

/**
     * 初始化
     *
     * @param packageNames
     */
    private void instance(List<String> packageNames) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        if (packageNames.size() < 1) {
            return;
        }
        for (String packageName : packageNames) {
            Class<?> fileClass = Class.forName(packageName);
            if (fileClass.isAnnotationPresent(MyController.class)) {
                MyController myController = fileClass.getAnnotation(MyController.class);
                final String controllerName = myController.name();
                instanceMap.put(controllerName, fileClass.newInstance());
                nameMap.put(packageName, controllerName);
                System.out.println("Controller:" + packageName + "name" + controllerName);
            } else if (fileClass.isAnnotationPresent(MyService.class)) {
                final MyService service = fileClass.getAnnotation(MyService.class);
                final String serviceName = service.name();
                instanceMap.put(serviceName, fileClass.newInstance());
                nameMap.put(packageName, serviceName);
            } else if (fileClass.isAnnotationPresent(MyRepository.class)) {
                final MyRepository repository = fileClass.getAnnotation(MyRepository.class);
                final String repositoryName = repository.name();
                instanceMap.put(repositoryName, fileClass.newInstance());
                nameMap.put(packageName, repositoryName);
            }
        }
    }

 

3.2.9:springIoc

springIoc的注入:該方法的目的主要是遍歷instanceMap,找到有@MyController、@MyService、@MyRepository中的成員欄位上有@Myqualifer的註解,(注:Myqualifer的作用類似於@AutoWired自動注入)然後獲取這個註解,利用反射機制,例項化這個成員變數。

    /**
     * springIOC的注入
     *
     * @throws IllegalAccessException
     */
    private void springIoc() throws IllegalAccessException {
        for (Map.Entry<String, Object> annotationNameAndInstance : instanceMap.entrySet()) {
            final Field[] fields = annotationNameAndInstance.getValue().getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(MyQualifer.class)) {
                    final String myQualiferName = field.getAnnotation(MyQualifer.class).name();
                    field.setAccessible(true);
                    field.set(annotationNameAndInstance.getValue(), instanceMap.get(myQualiferName));
                }
            }
        }
    }

3.3.0:handleUrlMethodMap

這個方法的主要作用就是獲取@MyRequestMapping中配置的url連結,然後將他們封裝到一個methodPackageMap的map中,其首先是取類上面的@MyRequestMapping配置的url,然後再取方法上的@MyRequestMapping配置的url,拼接在一起,作為鍵,對映其對應的方法,這樣當請求進來時,就可以直接根據url匹配到對應的處理的方法。向下面的例項:methodPackageMap將會存放的資料是("/orderApi/create",create(method)),而methodPackageMap將會存放的是(create,"com.wyq")

/**
     * 處理請求的url和method
     * @throws ClassNotFoundException
     */
    private void handleUrlMethodMap() throws ClassNotFoundException {

        if (packageNames.size() < 1) {
            return;
        }

        for (String packageName : packageNames) {

            final Class<?> fileClass = Class.forName(packageName);

            final Method[] methods = fileClass.getMethods();

            final StringBuffer baseUrl = new StringBuffer();

            if (fileClass.isAnnotationPresent(MyRequestMapping.class)) {

                MyRequestMapping myRequest = (MyRequestMapping) fileClass.getAnnotation(MyRequestMapping.class);
                baseUrl.append(myRequest.name());
            }

            for (Method method : methods) {
                if (method.isAnnotationPresent(MyRequestMapping.class)) {
                    final MyRequestMapping myrequest = method.getAnnotation(MyRequestMapping.class);
                    baseUrl.append(myrequest.name());

                    urlMethodMap.put(baseUrl.toString(), method);
                    methodPackageMap.put(method, packageName);
                }
            }
        }
    }

3.3.1:doPost方法

  這個方法的主要的思路就是把上面組裝的map資料然後再反向解析,首先就是獲取請求的url,然後獲取contextPath(專案的地址),比如請求http://172.123.344.122:8080/order/create,那麼最終的path就是order/create,再根據請求的url獲取對應的處理方法,這裡會找到create方法,再根據方法名找到類所在的包路徑,再根據包路徑獲取Controller的名字,再根據Controller的名字獲取具體的Controller(使用者寫的),再利用反射機制呼叫具體的Controller裡的方法,這樣從請求進入到解析處理就完成了!

/**
     * 具體的處理請求的方法
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        final String requestURI = req.getRequestURI();
        final String contextPath = req.getContextPath();
        final String path = requestURI.replaceAll(contextPath, "");
        //根據請求的路徑反向獲取方法
        final Method method = urlMethodMap.get(path);
        if (null != method) {
            //獲取包名
            final String packageName = methodPackageMap.get(method);
            //根據包名獲取到Controller的名字
            final String controllerName = nameMap.get(packageName);
            //根據controller的名字得到控制器
            Object orderController =  instanceMap.get(controllerName);
            try {
                method.setAccessible(true);
                method.invoke(orderController);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

四:總結

       本篇部落格主要是講解了簡易版SpringMvc的實現,其中主要包括了基本註解定義,還有springmv的具體處理模式,當然真實的springmvc實現很複雜,這裡只是冰山一角的簡單實現一個功能,不過舉一反三,基本思路我們要思考,雖然沒有搭建一個完成版springmvc的能力,不過從最簡單的實現開始,一步步的走,對我們的程式設計能力有推波助瀾的功效!加油