@copyright 杨同峰 保留所有权利
本文可以转载,但请保留版权信息。
SSH框架配置复杂、难用。个人认为这不是一个框架应该有的样子。框架应该使用简单、配置简单、代码简洁。于是参照Django的一些特性,编写了这个MVC+ORM框架。
<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> <context-param> <param-name>dbport</param-name> <param-value>3306</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。
public class IndexController extends Controller { public void index(){ output("Hello YangMVC"); } }
他的作用就是显示一句话。如图
第零个例子的显示
IndexController来处理应用的根目录下的请求。 index方法来处理这个目录下的默认请求。
在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/目录即可显示出结果
这里写图片描述
你作出的结果可能没那么好看,这完全取决于css。
在YangMVCDemo / WebRoot / view / book / mvc.css 中有一个漂亮的表格定义。
你可以通过类似下面的语句来加入到网页中
<link href="view/book/mvc.css" rel="stylesheet">
注意路径要对。
说明:
这个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")
是不是很方便??? 真的是非常方便的。。
添加书籍页面
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
好了,这几行代码就搞定了输入表单和表单的处理。
细心的朋友发现,我们是按照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的请举手。
public void del(){ Integer id = paramInt("id"); Model.tool("book").del(id); jump("index"); }
瞧瞧就这点代码了, 获取参数id,并调用tool的del方法删除。最后一句我们第一次见,就是跳转。跳转到同目录下的index这个默认页(显示的是书籍列表)
控制器是一个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类)和一个网址建立关联。 用户访问某网址时,框架自动调用控制器的某个函数。
因为本框架设计思想希望配置尽可能少,所以这里的路径映射是通过命名关系的。
假设应用的根目录为
http://localhost:8080/YangMVC/
如在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方法。
获取request中的参数
String s = param("name"); Integer id = paramInt("id");
输出方法
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)。
获取参数的方法
检查方法
public void checkNull(String name,Object obj)
检查obj是否为null,如果是抛出NullParamException异常。
Model与DBTool
Model 对象对应数据库的表格,它会与一个表格进行绑定。DBTool相当于是它的DAO类。
YangMVC的ORM组件可以单独使用。使用前需要先配置数据库:
Config.dbhost = "localhost"; Config.dbname = "dc2"; Config.dbpwd = "123456"; Config.dbusr ="root"; Config.dbport="3306";
也可以和MVC框架一起使用。配置时在web.xml中配置
创建一个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 '%王%'
根据原始sql获取(version >=1.5.4)
LasyList list = LasyList.fromRawSql("select name from book");
使用原始的sql获取的List中的模型将和数据库表没有关联。
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)
设置某一个属性的值
多 ASP.NET 开发人员开始接触 MVC,都认为 MVC 与 ASP.NET 完全没有关系,是一个全新的 Web 开发。
事实上 ASP.NET 是创建 WEB 应用的框架,而 MVC 是一种能够用更好的方法来组织并管理代码的体系,所以可以称之为 ASP.NET MVC。
因此,我们可以将原来的 ASP.NET 称为 ASP.NET Webforms,新的 MVC 称为 ASP.NET MVC。
ASP.NET Webforms
ASP.NET 在过去的十几年里,已经服务并成功实现Web 应用的开发。那么,我们先了解一下为什么ASP.NET能够如此流行,并成功应用?
微软编程语言从 VB 开始就能够成为流行并广泛应用,都源于其提供的强大的 Visual studio 能够进行可视化的编程,实现快速开发。
使用 VS 时,开发人员能够通过拖拽 UI 元素,并在后台自动生成这些界面的代码,称为后台代码。在后台代码中,开发人员可以添加操作这些UI元素的逻辑代码。
因此微软的可视化 RAD 架构体系有两方面组成,一方面是 UI,一方面是后台代码。
ASP.NET WebForms 存在的问题
响应时间
如图所示,每一次 WebForms 请求都有转换逻辑,运行并转换服务器控件为 HTML 输出。如果页面使用表格,树形控件等复杂控件,转换就会变得很糟糕,HTML 输出也是非常复杂的。由于这些不必要的转换从而增加了响应时间。上图是 ASP.Net MVC 和 Webforms 的响应时间对比,我们会发现 Webforms 的响应时间是 MVC 的两倍左右。
带宽消耗
ASP.NET 开发人员都非常熟悉 Viewstates,因为它能够自动保存 post 返回的状态,减少开发时间。但是这种开发时间的减少会带来巨大的消耗,Viewstate增加了页面的大小。
从上图中,我们可以看到与 MVC 对比,Viewstate 增加了两倍的页面存储。
MVC是怎么弥补这些问题的?
Asp.Net MVC 由 Model,View,Controller 三部分组成。Controller 中包含后台代码逻辑,View 是ASPX,如纯 HTML 代码,Model 是中间层。
不使用服务器控件,直接编写 HTML 代码,并且将后台代码迁移到独立的类库中,是 MVC 解决 Webforms 问题的方法。
直接编写HTML代码的好处在于,web设计者可以与开发人员紧密合作及时沟通,设计人员也可以使用他们喜爱的设计工具来设计HTML代码。
将后台代码迁移到独立的简单的类库,执行效率也会大大提高。
ASP.NET Webform 和 MVC 比较,如上图所示。
深入理解 ASP.NET MVC 今天就讲到这里,后续还会更新 “七天学会 ASP.NET MVC” 的其它篇章。
敬请期待!
相关开发工具
要进行 ASP.ET MVC 的开发,不但需要具备 MVC 的知识,还需要高效的工具来帮助开发。
使用 ComponentOne Studio Enterprise 中提供的 ComponentOne Studio ASP.NET MVC,您能获取快速的轻量级控件来满足用户所有需求,大大减轻工作量。
快人一步,免费试用
如果您想试用 ComponentOne Studio ASP.NET MVC,请联系我们:
微信:GrapeCityDT
邮件:marketing.xa@grapecity.com
官网:www.gcpowertools.com.cn
关于葡萄城控件
葡萄城是一家跨国软件研发集团,专注控件领域近30年,是全球最大的控件提供商,也是微软认证的金牌合作伙伴
下文章来源于非正式解决方案 ,作者winlion
非正式解决方案
思考链接价值,非正式解决方案,既扯高大上如人工智能、大数据,也关注码农日常如分布式、java和golang,每天分享瞎想的东西。
目录结构说明如下
名称内容model模型层目录,类比Java 中的entityview视图层,存放所有templete模板ctrl控制器层, 存放全部控制器service服务层,类比Java里面的servicehtml一些静态资源页面util核心工具包,Md5加密,返回数据封装等asset静态资源目录,存放js/css/image等args封装全部请求参数对象mnt上传文件的存放目录app.dev.conf开发环境配置文件app.prod.conf生产环境配置文件start.sh/start.bat启动脚本build.sh/build.bat打包脚本main.go主应用程序文件
主程序主要做各种初始化工作
func main() {
//解析配置文件
fpath := flag.String("c","app.dev.conf","config file path")
flag.Parse()
_,err:=util.Parse(*fpath)
if err!=nil{
fmt.Sprintf("error when %s",err.Error())
return
}
//配置日志
logmap := util.GetSec("log")
service.InitLog(logmap)
//初始化数据库
dbmap := util.GetSec("database")
service.InitDb(dbmap)
//注册funcmap
ctrl.RegisterFuncMap()
//控制器
ctrl.RegisterCtrl()
//静态资源文件
fileservermap := util.GetSec("fileserver")
ctrl.InitFileServer(fileservermap)
//初始化session
sessioncfg:=util.GetSec("session")
util.StartSession(sessioncfg)
appcfg := util.GetSec("app")
//视图控制器
ctrl.RegisterView(appcfg)
fmt.Println("http ListenAndServe " + appcfg["addr"])
//打开服务器监听http
err = http.ListenAndServe(appcfg["addr"], nil)
if err!=nil{
fmt.Println(err.Error())
log.Println(err.Error())
}
}
使用配置文件开发包,如github.com/Unknwon/goconfig 包。
//util/config.go
var cfg *goconfig.ConfigFile
var cfgmap map[string]map[string]string = make(map[string]map[string]string)
var filepath string
//解析peiz
func Parse(fpath string)(c map[string]map[string]string ,err error){
cfg, err := goconfig.LoadConfigFile(fpath)
filepath = fpath
sec :=cfg.GetSectionList()
for _,v :=range sec{
cfgmap[v]=make(map[string]string,0)
keys := cfg.GetKeyList(v)
for _,b:= range keys{
cfgmap[v][b],_ = cfg.GetValue(v,b)
}
}
return cfgmap,err
}
//全部都存放在存放
func GetAllCfg()(c map[string]map[string]string){
return cfgmap
}
//重新刷新配置文件
func ReloadAllCfg()(c map[string]map[string]string){
return return Parse(filepath)
}
调用案列
util.GetAllCfg()["app"]["port"]
使用github.com/fsnotify/fsnotify包,装时候注意,一个函数里面如果有参数共享,应该放到一个携程里。
//监听文件
func WatchConfig(filepath ...string) {
//创建一个监控对象
go func() {
watch, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watch.Close()
//添加要监控的对象,文件或文件夹
for _, fpath := range filepath {
err = watch.Add(fpath)
if err != nil {
log.Fatal(err)
}
fmt.Println("WatchConfig " + fpath)
}
for {
select {
case ev := <-watch.Events:
{
if ev.Op&fsnotify.Write == fsnotify.Write {
//监听到文件系统使用加载新东西
ReloadAllCfg()
}
fmt.Println(ev.Op, ev.Name)
}
case err := <-watch.Errors:
{
log.Println("error : ", err)
return
}
}
}
}()
}
fsnotify 支持很多种事件监听,一般在 Write 事件刷新配置文件
//判断事件发生的类型,如下5种
// Create 创建
// Write 写入
// Remove 删除
// Rename 重命名
// Chmod 修改权限
if ev.Op&fsnotify.Create == fsnotify.Create {
log.Println("创建文件 : ", ev.Name);
}
if ev.Op&fsnotify.Write == fsnotify.Write {
log.Println("写入文件 : ", ev.Name);
}
if ev.Op&fsnotify.Remove == fsnotify.Remove {
log.Println("删除文件 : ", ev.Name);
}
if ev.Op&fsnotify.Rename == fsnotify.Rename {
log.Println("重命名文件 : ", ev.Name);
}
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
log.Println("修改权限 : ", ev.Name);
}
系统级配置参数 假设修改了会影响整个应用,需要另起服务的我们称之为系统级配置,修改了参数,往需要进行相应的操作。如修改了数据库连接地址,需要重置数据库连接操作。修改了应用服务器端口,则需要重启应用服务。
用户级配置参数 如微信公众号 appsecret,每次调用的时候会从配置中获取,因此只需要重新加载数据即可。
需要将配置文件内容缓存到 map 中。 需要考虑到 map 的并发操作。
//model/user.go
//用户性别和角色
const (
WOMEN=2
MAN=1
Unknow=0
ROLE_ADMIN =1
ROLE_USER=0
)
type User struct {
Id int64 `xorm:"pk autoincr BIGINT(20)" form:"id" json:"id"`
NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`
Openid string `xorm:"VARCHAR(40)" form:"openid" json:"openid"`
Mobile string `xorm:"VARCHAR(15)" form:"mobile" json:"mobile"`
Passwd string `xorm:"VARCHAR(40)" form:"passwd" json:"-"`
Role int `xorm:"int(11)" form:"role" json:"role"`
Enable int `xorm:"int(11)" form:"enable" json:"enable"`
Gender int `xorm:"int(11)" form:"gender" json:"gender"`
}
在如上代码中,常用角色变量 ROLE_USER和ROLE_ADMIN 定义在同一个文件中,便于阅读。
实体和表结构对应,一定要定义 Form 和 Json tag。这样可以提高系统适用性,为什么呢?因为可以适配前端以各种 Content-Type 提交数据如。后端统一用该实体接收数据即可。
//urlencode类
application/x-www-form-urlencoded格式
mobile=18273252300&passwd=123456
//json类
application/x-www-form-urlencoded格式
{"mobile":"18273252315","passwd":"123456"}
约定统一使用驼峰式或者下划线标记。如下,建议使用驼峰式。
#驼峰
NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`
#下划线
NickName string `xorm:"VARCHAR(40)" form:"nick_name" json:"nick_name"`
如下几点需要注意
关联便于代码管理和阅读。模板位置 /view/demo/index.html,模板内容如下。
{{define "demo/index"}}
<div>
Hello,Modal
</div>
{{end}}
外部调用方法如下,大家能很自然知道模板文件位置。
http://localhost/demo/index
主要是为了程序员生活更美好(早点下班+偷懒)。
//ctrl/base.go
func RegisterPage(isDev bool) {
//初始化一个全局的模板变量
GlobTemplete := template.New("root")
//把一些函数添加进去,这样页面里面就可以使用函数啦
GlobTemplete.Funcs(GetFuncMap())
//解析模板 ,demo/index => 模板
GlobTemplete, err := GlobTemplete.ParseGlob("view/**/*")
for _, templete := range GlobTemplete.Templates() {
tplname := templete.Name()
patern := "/" + tplname
fmt.Printf("register templete %s ==> %s\n", patern, tplname)
//这里就是 /demo/index 这个url 和对应的处理函数之间的关系
http.HandleFunc(patern, func(w http.ResponseWriter, req *http.Request) {
fmt.Println(patern + "=>" + tplname)
if isDev {
GlobTemplete := template.New("root")
GlobTemplete.Funcs(GetFuncMap())
GlobTemplete, err = GlobTemplete.ParseGlob("view/**/*")
for _, v := range GlobTemplete.Templates() {
if v.Name() == tplname {
templete = v
}
}
}
err = templete.ExecuteTemplate(w, tplname, nil)
if err != nil {
fmt.Println(err.Error())
}
})
}
}
//在main.go中初始化
func main(){
///
ctrl.RegisterPage(true)
//
}
外部调用方法如下,大家能很自然知道模板文件位置。
http://localhost/demo/index
为什么要添加调试模式支持?因为调试模式状态下,我们修改来了页面模板,需要立即看到页面内容,而不需要重启应用。核心代码如下,即在调试模式状态下,每次请求都重新解析模板。
if isDev {
GlobTemplete := template.New("root")
GlobTemplete.Funcs(GetFuncMap())
GlobTemplete, err = GlobTemplete.ParseGlob("view/**/*")
for _, v := range GlobTemplete.Templates() {
if v.Name() == tplname {
templete = v
}
}
}
由上可见,调试模式效率是非常低的,我们不应该在生产环境采用调试模式。
应用场景是在每个页面中都需要使用 session 中的用户 ID 数据。方法是在 RegisterPage 函数内部模板templete.ExecuteTemplate(w, tplname, nil)处秀修改成如下代码
//从session中获取用户信息
user := loadDataFromSession(req)
err = templete.ExecuteTemplate(w, tplname, user)
前端模板调用代码如下
{{define "demo/index"}}
<div>
Hello,Modal ,User id is {{.Id}}
</div>
{{end}}
返回结果
Hello,Modal ,User id is xxx
在 RegisterPage 方法内定义一个 funMap
//ctrl/funcmap.go
var resFuncMap template.FuncMap = make(template.FuncMap)
func hello (){
return "hello"
}
func hello2 (test string){
return "hello" + test
}
//初始化方法
func RegisterFuncMap(){
resFuncMap ["hello"]=hello
}
main.go 中初始化
//在main.go中初始化
func main(){
///
ctrl.RegisterFuncMap()
//
}
前端模板调用代码如下
{{define "demo/index"}}
<div>
Hello,Modal ,hello func retutn {{hello}}
Hello,Modal ,hello2 func retutn {{hello2 "参数2"}}
</div>
{{end}}
返回结果
Hello,Modal ,hello func retutn hello
Hello,Modal ,hello func retutn hello2参数2
主要使用场景是分角色菜单,用户
{{define "demo/memo"}}
{{if eq .Role 1}}
菜单内容1
{{else if eq .Role 2}}
菜单内容2
{{end}}
<script>
GLOB={"ROLE":.Role}
</script>
{{end}}
其他页面统一调用,进行角色菜单等控制。
{{define "demo/index"}}
<div>
{{templete "demo/menu"}}
Hello,Modal ,hello func retutn {{hello}}
Hello,Modal ,hello2 func retutn {{hello2 "参数2"}}
</div>
{{end}}
控制器层主要处理对外接口
import (
///很多包
)
//定义个一个控制器对象
type UserCtrl struct {
}
//将url和处理函数绑定
func ( ctrl *UserCtrl)Router(){
Router("/user/login",ctrl.authwithcode)
}
//定义用户处理函数
var userService service.UserService
//用户通过小程序登录处理函数,输入code
//通过util.RespOk 或者util.RespFail输出
func( ctrl * UserCtrl)authwithcode(w http.ResponseWriter, req *http.Request) {
var requestdata args.AuthArg
util.Bind(req,&requestdata)
cfgminapp := util.GetSec("miniapp")
resp,err := util.Code2Session(cfgminapp["appid"],cfgminapp["secret"],requestdata.Code)
if err!=nil{
util.RespFail(w,err.Error())
return
}
requestdata.User.Openid = resp.Openid
requestdata.User.SessionKey = resp.SessionKey
u,err:= userService.LoginWithOpenId(requestdata.User)
if err!=nil{
util.RespFail(w,err.Error())
}else{
util.RespOk(w,model.User{
Ticket:u.Ticket,
ClientId:u.ClientId,
Role:u.Role,
})
}
}
所有参数都需要可预期在一个结构体里面。这样整个系统编程将变得非常简单。在上 面函数中,通过如下代码实现参数绑定
var requestdata args.AuthArg
util.Bind(req,&requestdata)
其中 args.AuthArg 对象定义如下
package args
import "../model"
type AuthArg struct {
PageArg
model.User
Code string `json:"code" form:"code"`
Kword string `json:"kword" form:"kword"`
Passwd string `json:"passwd" form:"passwd"`
}
args 作用是存放一切请求参数。每个业务都建议定义一个 arg。每个 arg 都有一个公共属性 PageArg。PageArg 定义如下
import (
"fmt"
"time"
)
//常用的搜索,大家可以自行添加
type PageArg struct {
Pagefrom int `json:"pagefrom" form:"pagefrom"`
Pagesize int `json:"pagesize" form:"pagesize"`
Kword string `json:"kword" form:"kword"`
Asc string `json:"asc" form:"asc"`
Desc string `json:"desc" form:"desc"`
Stat int `json:"stat" form:"stat"`
Datefrom time.Time `json:"datafrom" form:"datafrom"`
Dateto time.Time `json:"dateto" form:"dateto"`
Total int64 `json:"total" form:"total"`
}
//获得分页大小
func (p*PageArg) GetPageSize() int{
if p.Pagesize==0{
return 100
}else{
return p.Pagesize
}
}
//获得分页当前第几页
func (p*PageArg) GetPageFrom() int{
if p.Pagefrom<0{
return 0
}else{
return p.Pagefrom
}
}
//获得排序 ID DESC ,前端传递参数 desc=排序字段 或者asc=排序字段
func (p*PageArg) GetOrderBy() string{
if len(p.Asc)>0{
return fmt.Sprintf(" %s asc",p.Asc)
} else if len(p.Desc)>0{
return fmt.Sprintf(" %s desc",p.Desc)
}else{
return ""
}
}
大体结构如下
func Bind(req *http.Request,obj interface{}) error{
contentType := req.Header.Get("Content-Type")
//如果是简单的json,那么直接用JSON解码
if strings.Contains(strings.ToLower(contentType),"application/json"){
return BindJson(req,obj)
}
//如果是其他的urlencode那么就用BindForm去处理
if strings.Contains(strings.ToLower(contentType),"application/x-www-form-urlencoded"){
return BindForm(req,obj)
}
//可以自行扩展xml
if strings.Contains(strings.ToLower(contentType),"text/xml"){
return BindXml(req,obj)
}
return errors.New("当前方法暂不支持")
}
以 BindJson 为例子
func BindJson(req *http.Request,obj interface{}) error{
s, err := ioutil.ReadAll(req.Body) //把 body 内容读入字符串
if err!=nil{
return err
}
err = json.Unmarshal(s,obj)
return err
}
可能大家更关心 BindForm,篇幅太长,大家可以移步
https://www.github/winlion/restgo-admin
一般封装一个底层 JSON,然后根据返回成功或失败响应对应的 code
/util/resp.go
package util
import (
"net/http"
"encoding/json"
"fmt")
//定义个通用的结构体用于装载返回的数据
type H struct {
Code int `json:"code"`
Rows interface{} `json:"rows,omitempty"`
Data interface{} `json:"data,omitempty"`
Msg string `json:"msg,omitempty"`
Total interface{} `json:"total,omitempty"`
}
//返回Json的底层方法
func RespJson(w http.ResponseWriter,data interface{}){
header :=w.Header()
header.Set("Content-Type","application/json;charset=utf-8")
w.WriteHeader(http.StatusOK)
ret,err :=json.Marshal(data)
if err!=nil{
fmt.Println(err.Error())
}
w.Write(ret)
}
//当操作成功返回Ok,
func RespOk(w http.ResponseWriter,data interface{}){
RespJson(w,H{
Code:http.StatusOK,
Data:data,
})
}
//当操作失败返回Error,
func RespFail(w http.ResponseWriter,msg string){
RespJson(w,H{
Code:http.StatusNotFound,
Msg :msg,
})
}
以订单管理为例
package service
import (
"../model"
"../args"
"github.com/go-xorm/xorm"
"log"
"github.com/pkg/errors"
"encoding/json"
"time"
)
type OrderService struct {
}
//构造条件
func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session){
orm := DBengin.Where("id > ?",0)
if(!arg.Datefrom.IsZero()){
orm = orm.And("createat >= ?",arg.Datefrom.String())
}
if(!arg.Dateto.IsZero()){
orm = orm.And("createat <to ?",arg.Datefrom.String())
}
if (arg.Seller>0){
orm = orm.And("seller = ?",arg.Seller)
}
if (arg.Buyer>0){
orm = orm.And("buyer = ?",arg.Buyer)
}
if (arg.Stat>0){
orm = orm.And("stat = ?",arg.Stat)
}
return orm
}
//增加
func (service *OrderService) Create(order model.Order) (model.Order,err){
_,err = DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
//删除
func (service *OrderService) Delete(order model.Order) (error){
return nil
}
//修改
func (service *OrderService) Create(order model.Order) (model.Order,err){
_,err = DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
//搜索
func (service *OrderService) Search(orderArg args.OrderArg) ([]model.Order, error){
var ret []model.Order = make([]model.Order,0)
return ret,nil
}
//查询某一个
func (service *OrderService) Create(order model.Order) (model.Order){
_,err := DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
一般需要构建如下几类函数,具体随业务而定
名称内容Create添加Update修改Search搜索,返回列表Find返回某一个对象Delete删除buildCond构建条件函数Count符合某一条件的记录数目
我们可以用类似于如下函数来统一管理查询条件,该函数输出参数,输出一个 session。
func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session)
条件规范化可以让应用更灵活,让业务更清晰。如果不规范,楼主曾经经历的教训可能也会撞上你。
数据库建议使用 xorm。 在 server 包目录下新建 init.go 在其中实现数据库的初始化
//定义全局变量DBengin
var DBengin *xorm.Engine
//定义初始化函数InitDb,dbmap是数据库配置参数,=来自于外部参数
func InitDb(dbmap map[string]string){
driverName := dbmap["driveName"]
dataSourceName := dbmap["dataSourceName"]
showsql := dbmap["showSql"]!="false"
maxIdle,_ := strconv.Atoi(dbmap["maxIdle"])
maxOpen,_ := strconv.Atoi(dbmap["maxOpen"])
sync := dbmap["sync"]=="true"
dbengin , err := xorm.NewEngine(driverName, dataSourceName)
if err != nil {
panic("data source init error ==>"+err.Error())
}
if sync{
dbengin.Sync2(new(model.User),
new(model.Item),
new(model.Order),
new(model.User),
)
}
dbengin.ShowSQL(showsql)
dbengin.SetMaxIdleConns(maxIdle)
dbengin.SetMaxOpenConns(maxOpen)
dbengin.SetConnMaxLifetime(5*time.Second)
DBengin = dbengin
}
main.go 中初始化数据库
func main(){
//
dbmap = util.GetSec("database")
server.InitDb(dbmap)
//
}
具体使用可以参考 Xorm
func (service *OrderService) Create(order model.Order) (model.Order){
//就是这么用的
_,err := DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
在每一个 ctrl 中都定义一个 Router 函数
func ( ctrl *UserCtrl)Router(){
Router("/open/register",ctrl.Register)
Router("/open/authwithpwd",ctrl.authwithpwd)
Router("/user/find",ctrl.Find)
Router("/user/quit",ctrl.quit)
Router("/open/authwithcode",ctrl.authwithcode)
}
这些函数调用了 Router 方法,该方法本质上是对 http.HanderFunc 的封装
//ctrl/base.go
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
fun(w, req)
})
}
定义路由注册函数
//注册控制器
func RegisterCtrl() {
new(UserCtrl).Router()
new(OpenCtrl).Router()
new(AttachCtrl).Router()
}
注册路由 在 main.go 中完成路由注册
func main(){
//
ctrl.RegisterCtrl()
//
}
解决思路如下 首先在 ctrl/base.go 里面定义一个 map
PostRouterMap := make(map[string]HandFunc)
GetRouterMap := make(map[string]HandFunc)
接着定义路由绑定函数
type Handlefunc func(w http.ResponseWriter,req *http.Request)
func Post(formate string,handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,func(w http.ResponseWriter,req *http.Request){
if req.Method==http.MethodPost {
handlefunc(w,req)
}else{
//not sourport 处理
}
})
}
func Get(formate string,
handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,
func(w http.ResponseWriter,req *http.Request){
if req.Method==http.MethodGet {
handlefunc(w,req)
}else{
//not sourport 处理
}
})
}
//支持任意方式
func Any(formate string,
handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,
func(w http.ResponseWriter,req *http.Request){
handlefunc(w,req)
})
}
首先需要定义默认路由。RegisterRegExRouter() 中定义了默认路由 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request)。任何找不到的路由都会进入这个。
//这一个专门存uri和处理函数之间关系的字典
var RegExRouterMap map[string]func(w http.ResponseWriter, req *http.Request) = make(map[string]func(w http.ResponseWriter, req *http.Request), 0)
//这是一个存储Uri和对应正则表达式的字典以后就不要编译啦。
var RegexpMatchMap map[string]*regexp.Regexp = make(map[string]*regexp.Regexp, 0)
func RegExRouter(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
RegExRouterMap[pantern] = fun
//形成映射关系
RegexpMatchMap[pantern],_ = regexp.Compile(pantern)
}
//没有找到需要一个默认404
func notfound(w http.ResponseWriter, req *http.Request){
w.Write([]byte("404 NOT FOUNT"))
}
func RegisterRegExRouter(){
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
uris := strings.Split(req.RequestURI,"?")
uri := uris[0]
handlefunc := notfound
for p,regm := range RegexpMatchMap{
if regm.MatchString(uri){
handlefunc = RegExRouterMap[p]
break
}
}
handlefunc(w,req)
})
}
在路由注册中初始化
//注册控制器
func RegisterCtrl() {
//new(AttachCtrl).Router()
RegisterRegExRouter()
}
现在我们可以在控制器页面通过 RegExRouter 添加正则路由啦
//ctrl/user.go
func (ctrl *UserCtrl) Router() {
Router("/open/authwithcode", ctrl.authwithcode)
RegExRouter("/d/.*", ctrl.regtext)
}
func (ctrl *UserCtrl) regtext(w http.ResponseWriter, req *http.Request) {
util.RespOk(w, req.RequestURI)
}
客户端请求
http://localhost/d/12345678977
响应数据
{"code":200,"data":"/d/12345678977"}
在如上所示中定义了 notfound 函数,当没有任何一个匹配对象时候,进入这个函数。
//没有找到需要一个默认404
func notfound(w http.ResponseWriter, req *http.Request){
w.Write([]byte("404 NOT FOUNT"))
}
我们可以在 Router 方法里面实现拦截器功能,主要用来做鉴权,日志记录等
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
//包含某些关键字的不需要鉴权啦
if strings.Contains(req.RequestURI, "/test/") {
fun(w, req)
} else {
//否则判断一下,如果从xxxplatform平台来的不需要鉴权,直接往下走
ticket := req.Header.Get("request-ticket")
clientid := req.Header.Get("request-clientid")
platform := req.Header.Get("request-platform")
if platform != "xxxplatform" {
fun(w, req)
return
}
//否则这要鉴权,通过就直接往下走
if userService.Authorization(ticket, clientid) {
fun(w, req)
} else {
//没通过返回木有权限。
util.RespFail(w, "没有权限")
}
}
})
fmt.Printf("register patern %s ==> %s\n", pantern, pantern)
}
我主要在 Router 函数上下功夫,一种可用的设计是利用携程,如下
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
//先copy出来
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = ioutil.ReadAll(req.Body)
}
// 把刚刚读出来的再写进去
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
//这样就可以利用携程干事情啦
go fun(w, req)
})
}
需要注意的是要先把数据 copy 出来,然后才能利用携程,否则 fun 函数里面取出的数据是空的。
session、日志,可以引用第三方包。 鉴权可以参考拦截器。 安全,防 xss 攻击可以参考拦截器。 代码获取在公众号回复:golang框架
*请认真填写需求信息,我们会在24小时内与您取得联系。