高效快捷簡便易用的基於JSP的開源框架 MVC+ORM框架- YangMVC
專案地址
開發目的
@copyright 楊同峰 保留所有權利
本文可以轉載,但請保留版權資訊。
本人高校教師,帶著一門動態網站設計課程,前面講HTML+CSS+DIV,後面將JSP+JDBC+Struts+Hibernate+Spring。對SSH的難用深有體會。從一個空白專案開始配置完SSH,需要不少時間。如果你有模板當然binggo就好了。但。。。
這不是一個框架應該有的樣子。框架應該使用簡單、配置簡單、程式碼簡潔。總之,我思考了一個晚上後決定自己寫一個MVC+ORM(資料庫與程式語言的類建立對映就是ORM,寫給小朋友)
於是參照Django的一些特性,編寫了這個MVC+ORM框架。
這一稿程式碼從學生答辯完到現在花了一天半時間。
特性
- 大量的預設約定,避免了大量的配置
- 使用此框架開發效率會很高
- 支援延遲載入技術的List
- 和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這個預設頁(顯示的是書籍列表)
配置
- 新建一個Web
- Project(MyEclipse為例)
- 將以下三個jar放到WebRoot/Web-INF下面
yangmvc-1.0.jar
fastjson-1.2.0.jar
mysql-connector-java-5.1.23-bin.jar - 在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)。
獲取引數的方法
- param(String p) 獲取引數p的值,以String型別返回
- paramInt(String p) 獲取引數p的值,以Int型別返回,如果不是整數,則會出現異常
- public Model paramToModel(Model m)
根據名稱匹配的原則,將與模型中引數名相同的引數的值放入模型中。並返回該模型。
是收集表單資料到模型中的神器,手機後就可以直接進行資料庫操作了。 - 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)