1. 程式人生 > >高效快捷簡便易用的基於JSP的開源框架 MVC+ORM框架- YangMVC

高效快捷簡便易用的基於JSP的開源框架 MVC+ORM框架- YangMVC

專案地址

開發目的

@copyright 楊同峰 保留所有權利

本文可以轉載,但請保留版權資訊。

本人高校教師,帶著一門動態網站設計課程,前面講HTML+CSS+DIV,後面將JSP+JDBC+Struts+Hibernate+Spring。對SSH的難用深有體會。從一個空白專案開始配置完SSH,需要不少時間。如果你有模板當然binggo就好了。但。。。

這不是一個框架應該有的樣子。框架應該使用簡單、配置簡單、程式碼簡潔。總之,我思考了一個晚上後決定自己寫一個MVC+ORM(資料庫與程式語言的類建立對映就是ORM,寫給小朋友)
於是參照Django的一些特性,編寫了這個MVC+ORM框架。

這一稿程式碼從學生答辯完到現在花了一天半時間。

特性

  1. 大量的預設約定,避免了大量的配置
  2. 使用此框架開發效率會很高
  3. 支援延遲載入技術的List
  4. 和JSTL無縫相容

YangMVC的第零個例子-HelloWorld程式

public class IndexController extends Controller {
    public void index(){
        output("Hello YangMVC");
    }
}

他的作用就是顯示一句話。如圖
這裡寫圖片描述

IndexController來處理應用的根目錄下的請求。 index方法來處理這個目錄下的預設請求。

YangMVC第一個Demo

在org.demo包下建立此類:

public class BookController extends Controller {
    public void index(){
        DBTool tool = Model.tool("book");
        LasyList list = tool.all().limit(0, 30);
        put("books", list);
        render();
    }
}

在WebRoot/view/book/下建立一個index.jsp
其中核心的程式碼為

<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
(此處省略一堆無關的HTML程式碼) <table class="table table-bordered"> <c:forEach var="b" items="${books }"> <tr> <td>${b.id }</td> <td>${b.name }</td> <td>${b.author }</td> <td>${b.chaodai }</td> <td>${b.tm_year }</td> <td> <a href='book/edit?id=${b.id}'>編輯</a> <a href='book/del?id=${b.id}'>刪除</a> </td> </tr> </c:forEach> </table>

一個顯示列表的網頁就此搞定。訪問應用目錄下的book/目錄即可顯示出結果
這裡寫圖片描述

說明:

這個BookController是一個控制器,它的每一個公共方法都對應一個網頁(如果不想對應,你需要將其設為私有的)

Model和DBTool是整個ORM框架的核心。Model表示模型,它用來與資料庫表相對應。在建立一個Model時,會指定對應的表名。

這裡和Hibernate不同,Hibernate需要預先生成所有資料庫表的對應類, 而這個Model可以與任何表格關聯,而不需要預先生成任何一個類。 這正是YangMVC中的ORM的優勢所在。

DBTool tool = Model.tool("book");

程式中使用Model的靜態方法tool獲取一個DBTool物件,tool傳入的引數book是資料庫的表名。
這樣DBTool就和book表建立了關聯。

LasyList list = tool.all().limit(0, 30);

夥計們快看,這是個LasyList,一個支援懶惰載入機制的列表。它是List類的子類,這也就是它為什麼能在JSTL中使用foreach變數的原因。

首先我們呼叫了tool的all()方法,天哪,難道要載入book表的所有資料,兄弟不用害怕,在這個時候,它並沒有進行任何資料的讀寫,指示記錄了現在要訪問book表的所有資料這一資訊。 all()方法會返回一個LasyList物件。這麼設計的原因是我們後面可以跟一連串的過濾方法。方便我們程式設計。我們可以寫出這樣的東西:

list = tool.all().gt("id", 12).lt("id", 33).eq("name","haha").like("author","王");

這個例子相當於執行了如下SQL語句:

 select * from book where id>12 and id<33 and name='haha' and author like '%王%'

在上面的例子中, all()返回的LasyList又呼叫了它的limit方法,這一步仍然沒有真正訪問資料庫。
那麼訪問資料庫從哪裡開始呢? 從你獲取這個列表的一項時。

一個List,可以使用列舉的方法來訪問

for(Model m : list){

}

也可以使用get方法來訪問。如

Model m = list.get(12)

在你訪問具體它的一個元素(Model)時,資料庫查詢才會啟動。而且也不是將所有資料放到記憶體中。比如你通過上面for的方法列舉時,其實它是通過ResultSet的next遊標在移動,所以它很高效!也避免了無用的資料庫操作。

put("book",list)

該方法將查詢得到的book塞入request中,在jsp網頁中就可以使用JSTL來使用它。因為它是一個List,所以用forEach去訪問他。

Model 的一個物件對應於資料庫表的一行(一條記錄),Model是一個Map的子類!!!,所以在JSTL中,你可以使用
${ b.name } 的方式來訪問名為b的Model 的name項。 它相當於

   Model m = ....
   m.get("name")

是不是很方便??? 真的是非常方便的。。

第二個Demo

新增書籍頁面

    public void add(){
        DBTool tool = Model.tool("book");
        //處理提交資料
        if(isPost()){ //isPost
            Model m = tool.create(); //建立新的
            Log.d(m);
            paramToModel(m);
            tool.save(m);
            put("msg","新增成功");
        }

        //顯示資料
        renderForm(tool.create());
    }

對應的/view/book/add.jsp (這是預設對應的模板地址)的核心內容

  <div style="margin-left:100px">
  <h1>新增書籍 ${msg }</h1>
  ${book_form }
  </div>

這裡寫圖片描述

上面的例子控制器其實是對應兩個頁面。 在收到Get請求的時候顯示錶單,在使用者提交資料時,做插入操作,並顯示錶單。(我們當然可以把這兩個頁面寫到兩個不同的方法中)
我們還是使用Model.tool獲取一個DBTool。

先來看顯示錶單,就一句話

        renderForm(tool.create());

tool的create方法會返回一個Model物件,這個物件和book表相關聯(因為tool和book表關聯)。
並將這個Model傳遞給renderForm方法。這個方法會根據book表格的元資料自動建立一個表格。
哇偶!

那麼這個Form插入到網頁的什麼位置呢? 將 ${book_form } 放入網頁中 即可。

如果來的是POST請求(使用isPost()方法來判斷)
使用tool的create方法建立一個新的Model, 儘快還有其他建立Model物件的方式,但如果你希望插入,請儘量使用這種方式。
paramToModel(m) ,這個方法會自動查詢表單中,名字與資料庫欄位名匹配的項,並自動賦值給Model的相應項。是不是很方便。。。

想起了Struts那悲催的功能定義。 淚奔。。。。

隨後直接呼叫tool的save方法將其儲存到資料庫中!OK了!萬事大吉!

細心的小朋友會問: 資料庫中的欄位名都是英文的如name,為什麼在網頁上顯示的是中文???
看看我的資料庫表格定義

CREATE TABLE `book` (
  `id` int(11) NOT NULL auto_increment COMMENT '編號',
  `file_name` varchar(50) default NULL,
  `name` varchar(50) default NULL COMMENT '名稱',
  `author` varchar(50) default NULL COMMENT '作者',
  `chaodai` varchar(50) default NULL COMMENT '朝代',
  `tm_year` varchar(50) default NULL COMMENT '年代',
  `about` longtext COMMENT '簡介',
  `type` varchar(50) default NULL COMMENT '型別',
  `catalog_id` int(11) default NULL COMMENT '分類',
  PRIMARY KEY  (`id`),
  KEY `catalog` USING BTREE (`catalog_id`)
) ENGINE=InnoDB AUTO_INCREMENT=912 DEFAULT CHARSET=utf8;

真相大白與天下,我是通過給欄位加註釋實現的這一點。只要你將資料庫表格加上註釋,它就會自動獲取註釋並顯示,對於沒有註釋的欄位,則會顯示欄位名。如那個扎眼的file_name

好了,這幾行程式碼就搞定了輸入表單和表單的處理。

第三個demo-編輯(自動建立的修改表單)

細心的朋友發現,我們是按照CRUD的邏輯來將的。下面是編輯網頁。

    public void edit() throws NullParamException{

        DBTool tool = Model.tool("book");
        //處理提交資料
        if(isPost()){ //isPost
            Model m = tool.get(paramInt("id"));
            Log.d(m);
            paramToModel(m);
            tool.save(m);
            put("msg","修改成功");
        }

        //顯示資料
        Integer id = paramInt("id");
        checkNull("id", id);
        renderForm(tool.get(id));

    }

HTML頁面放在/view/book/edit.jsp中,核心程式碼只是將add.jsp中的新增二字改為了”編輯“二字。

  <div style="margin-left:100px">
  <h1>編輯書籍 ${msg }</h1>
  ${book_form }
  </div>

這個程式碼長了一點, 有17行。對於用YangMVC的,已經算夠長的了。它仍然是兩個網頁!!!
你可以吧顯示錶單的程式碼和處理表單的分到兩個方法中寫。

先看顯示資料。 首先使用paramInt方法獲取URL引數id,我們就是要編輯id指定的書籍。
呼叫checkNull來檢查一下。 在我的開發生涯中,遇到各種引數檢查,所以這個功能是必須有的,如果checkNull不過,就會丟擲一個異常。 這樣做的目的是不要讓這種引數檢查干擾我們正常的邏輯。這不就是異常之所以存在的意義麼?

如果缺少這個引數,頁面會提示說缺少這個引數。
下面使用tool.get(id)方法來獲取一個Model(一條記錄)。這個方法是根據表格的主鍵進行查詢,返回的不是列表而是一個具體的Model物件。在這裡我建議主鍵應當是整數、且是資料庫自增的。
renderForm傳入一個model,這個model中有資料,就會被顯示出來。

就這樣。編輯功能寫好了。

有的朋友問,如果不想用預設的表單怎麼辦? 那你自己寫一個表單在你的模板裡就是了。只不過,你可以先用這個方法吧表單生成出來,然後按你的意圖修改就成了。這也節省大量時間啊。做過Form的請舉手。

第四個DEMO-刪除

    public void del(){
        Integer id = paramInt("id");
        Model.tool("book").del(id);
        jump("index");


    }

瞧瞧就這點程式碼了, 獲取引數id,並呼叫tool的del方法刪除。最後一句我們第一次見,就是跳轉。跳轉到同目錄下的index這個預設頁(顯示的是書籍列表)

配置

  1. 新建一個Web
  2. Project(MyEclipse為例)
  3. 將以下三個jar放到WebRoot/Web-INF下面
    yangmvc-1.0.jar
    fastjson-1.2.0.jar
    mysql-connector-java-5.1.23-bin.jar
  4. 在web.xml中(web-app標籤內)加入
  <filter>
    <filter-name>yangmvc</filter-name>
    <filter-class>org.docshare.mvc.MVCFilter</filter-class>
    <init-param>
      <param-name>controller</param-name>
      <param-value>org.demo</param-value>
    </init-param>
    <init-param>
      <param-name>template</param-name>
      <param-value>/view</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>yangmvc</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <context-param>
    <param-name>dbhost</param-name>
    <param-value>localhost</param-value>
  </context-param>
  <context-param>
    <param-name>dbusr</param-name>
    <param-value>root</param-value>
  </context-param>
  <context-param>
    <param-name>dbpwd</param-name>
    <param-value>123456</param-value>
  </context-param>
  <context-param>
    <param-name>dbname</param-name>
    <param-value>mvc_demo</param-value>
  </context-param>

所有需要配置的都在這裡了。這裡做個簡要說明
MVCFilter是我們MVC框架的入口。(不管是啥MVC框架都免不了這個)
它有controller和template兩個引數。
controller 是你控制器存放位置的包名。 比如這裡是org.demo
template是你存放模板(檢視)的地方。這個路徑是相對於WebRoot即網站根目錄的。
比如這裡的配置(/view)是WebRoot下的view目錄。

dbhost dbname dbusr dbpwd 是資料庫的 地址、資料庫名、使用者名稱和密碼。目前這個MVC框架只支援MySQL,後續會新增其他資料庫的支援。

注意,模板目錄(template引數所配置的值)以/開頭,如/view。

控制器建立

控制器是一個Java類,類有若干方法。在YangMVC的設計中,控制器的每一個公共的方法都對映對應一個網頁。這樣一個Java類可以寫很多的網頁。 方便管理。(當然,你也可以在一個控制器中只寫一個方法來支援網頁,這沒問題(⊙﹏⊙)b)

所有的控制器都要繼承 org.docshare.mvc.Controller 這個類。充當控制器方法的方法應當是沒有引數沒有返回值的。如上面demo所示。

public class IndexController extends Controller {
    public void index(){
        output("Hello YangMVC");
    }
}

這些控制器都要寫在配置所制定的package中,或者子package中。如在上面的配置中

    <init-param>
      <param-name>controller</param-name>
      <param-value>org.demo</param-value>
    </init-param>

這個包為org.demo所有的控制器都要解除安裝這個包內。(你可以寫到外面,但它不會管用O(∩_∩)O~)

路徑對映

所謂路徑對映就是要將 一個控制器(一個Java類)和一個網址建立關聯。 使用者訪問某網址時,框架自動呼叫控制器的某個函式。

因為本框架設計思想希望配置儘可能少,所以這裡的路徑對映是通過命名關係的。

如在org.demo下(這個目錄可以在web.xml中配置,可見上一節)有一個BookController。
那麼這個類的路徑是 http://localhost:8080/YangMVC/book/
使用者訪問這個路徑時,框架會呼叫BookController 的index方法。如果沒有這個方法則會報錯。

index方法用以處理某個路徑下的預設網頁(網站以斜槓結尾的都會呼叫某個類的index方法來處理)。

book這個地址,將第一個字母大寫,後面追加Controller。於是
book (路徑名)-> Book -> BookController(類名)
這就是路徑和類名的預設關聯。

在這個網站後加入方法名可以訪問BookController的 任何一個公共方法。
http://localhost:8080/YangMVC/book/edit 與BookController的edit方法關聯。

需要注意的是,如果你寫的是 http://localhost:8080/YangMVC/book/edit/ (比上一個網站多了一個斜槓), 則它對應的是 book.EditController下的index方法 而不是BookController下的edit方法。

控制器方法

輸出方法

output方法

    output("Hello YangMVC");

這個方法輸出一個文字到網頁上(輸出流中),並關閉輸出流。因為它會關閉流,所以你不要呼叫它兩次。你如果需要輸出多次,以將內容放到StringBuffer中,然後統一輸出。

render方法

    public void paramDemo(){
        put("a", "sss");
        render("/testrd.jsp");

    }

這裡的testrd.jsp是模板目錄(/view)目錄下的。 /view/testrd.jsp
這裡的引數應該是相對於模板目錄的相對路徑。

render方法使用引數制定的網頁(一個包含JSTL的jsp檔案),將其輸出。可以通過put來制定引數。下面會詳細講。

render()方法

這個render方法是沒有引數的,它會使用預設模板,如果這個模板不存在,就會提示錯誤。
    public void renderDemo(){
        request.setAttribute("a", "sss");
        render();

    }

在配置 controller 為org.demo , template為/view 這種情況下。
org.demo.IndexController的renderDemo方法會對應/view/renderDemo.jsp
之所以模板存在於模板根目錄下,是因為這個IndexController是處理應用根目錄的。他們有對應關係。

如果是org.demo.BookController,它對應 app根目錄下的 /book/ 目錄。
它的add方法對應路徑 /book/add
如果應用名為hello,那麼完成路徑應該是 /hello/book/add

outputJSON 方法

該方法將引數轉化為JSON,並向網頁輸出。

    public void jsonDemo(){
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("id", 12);
        map.put("name", "Yang MVC");
        map.put("addtm",new Date());

        outputJSON(map);
    }
這個程式碼稍長,其實上面的所有都是生成一個Map,最後一句輸出。outputJSON可以輸出List,Map和任何Java物件。內部轉換是使用fastjson實現的。

自動生成並輸出一個表單

public void renderForm(Model m,String template,String postTo)

該函式會根據模型對應的表結構,自動生成一個表單,並將其內容放入 表格名_form 中,如book表會輸出到 book_form 中。
在網頁中,直接寫 ${book_form}就可以將表單放下去。

template制定對應的模板檔案,可以省略,省略後按照預設規則查詢模板檔案。
postTo設定 表單提交的網頁,可以省略,預設是”“,即當前網頁(Controller)。

獲取引數的方法

  1. param(String p) 獲取引數p的值,以String型別返回
  2. paramInt(String p) 獲取引數p的值,以Int型別返回,如果不是整數,則會出現異常
  3. public Model paramToModel(Model m)
    根據名稱匹配的原則,將與模型中引數名相同的引數的值放入模型中。並返回該模型。
    是收集表單資料到模型中的神器,手機後就可以直接進行資料庫操作了。
  4. paramWithDefault 獲取引數,但同時帶上預設值,如果沒這個引數則返回預設值。

檢查方法

public void checkNull(String name,Object obj)
檢查obj是否為null,如果是丟擲NullParamException異常。

ORM框架

Model與DBTool

Model 物件對應資料庫的表格,它會與一個表格進行繫結。DBTool相當於是它的DAO類。

建立一個DBTool物件

        DBTool tool = Model.tool("book");

其中book是資料庫表的名字。

建立一個空的Model

        DBTool tool = Model.tool("book");
        Model m = tool.create(); //建立新的

根據主鍵讀取一個Model

            Model m = tool.get(12);

查詢表中所有的行

        LasyList list = tool.all();
all返回一個LasyList物件。這個物件在此事並沒有真正進行資料庫查詢,只有在頁面真正讀取時才會讀取資料庫。這是它叫做Lasy的原因。此處借鑑了Django的實現機制。

查詢的limit語句

    LasyList list = tool.all().limit(30);
    list = tool.all().limit(10,30);

查詢的等式約束

    tool.all().eq("name","本草綱目")

查詢的不等式約束

    tool.all().gt("id",12) //id < 12
    tool.all().lt("id",33) //id <33
    tool.all().gte("id",12) //id>=12
    tool.all().lte("id",33) //id<=33
    tool.all().ne("id",33) //不相等

模糊查詢

    tool.all().like("name","本草")
查詢所有名字中包含本草的書。返回一個LasyList

排序

    tool.all().orderby("id",true);
按照id的增序排列。 如果是false,則是降序。

級聯查詢

因為這些上面的過濾器函式全部都會返回一個LasyList物件, 所以可以採用級聯的方式進行復雜查詢。如:

list = tool.all().gt("id", 12).lt("id", 33).eq("name","haha").like("author","王");

這個例子相當於執行了如下SQL語句:

 select * from book where id>12 and id<33 and name='haha' and author like '%王%'

Model的相關功能

model 是一個繼承自Map<String,Object> 的類,所以對於
Model m;
你可以在網頁中使用${m.name}的方式來訪問它的name鍵對應的值。相當於m.get(“name”)
這種寫法在JSTL中非常有用。讓Model繼承Map的初衷就在於此:方便在JSTL中使用。

大家也許注意到了LasyList是一個繼承自List<Model> 的類.
這就使得不管是LasyList還是Model在JSTL中訪問都極為的便利。

訪問所有的鍵值(即DAO物件的所有屬性)

    model.keySet();

訪問某一個屬性的值

    model.get(key)

設定某一個屬性的值

    model.put(key,value)