- 浏览: 264999 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
DragonKiiiiiiiing:
支持楼主,中国互联网太缺这种无私奉献的人了。您的这本书我已拜读 ...
JAVA NIO 全书 译稿 -
sp42:
非常感谢!热部署帮助很大!
Pure JS (2): 热部署 (利用 JDK 7 NIO 监控文件变化) -
sp42:
其实在我的架构中,我更倾向于 JSP 作为前端模板系统~还是写 ...
Pure JS (5.3):pure.render 的实现(构造window对象,实现服务器端 JQuery Template) -
sp42:
非常不错,楼主做的正是鄙人想做的做的,而且比鄙人的来的成熟、健 ...
OMToolkit介绍(5): 总结 -
cfanllm:
楼主辛苦了,,,谢谢分享啊
JAVA NIO 全书 译稿
OMToolkit: Web Server,Web Framework 及 Object-Orinted Database 的简单实现
- 博客分类:
- OMTookit
概述
最近在看一些NIO和concurrent的资料,写了一个小项目作为练习,看起来涉及面很广,但上实现方式很简单,整个项目共2179行。
项目已经上传到Google Code,地址:http://code.google.com/p/oh-my-toolkit/。可以在Downloads页面下载已经发布的0.0.1版本的OMToolikit以及OMSimpleBlog(一个示例,仅254行),也可以通过SVN下载源码。
OMToolkit除JDK 1.6之外不依赖于任何第三方包,本身包含了Web Server,Web Framework 和 Object-Orinted Database 三个部分, 以“Entity”概念作为提供给Web应用开发者的API。
最近将连续发布一系列文章,重点说明实现思路,同时也总结学习所得。
使用示例
首先以OMSimpleBlog为例,简要说明OMToolkit的使用方式。后续的系列文章将对这个例子进行更详细的说明。
1.Entity建模
Entity的属性分为两类,一类继承自OMField,包括StringField,DateField,Children等,这些属性将被持久化。
另一类为基本类型,如String,int等,这些属性实际上是视图属性,不会被持久化,但可以通过URL进行赋值,并在渲染对象时获取。
基于OMToolkit的Web应用必须有一个名为Database的Entity,OMSimpleBlog中,Database的建模方式如下:
public class Database extends Entity { private Children<User> userList; private References<Article> articleList; // Methods ... }
可以看到,Database包含了一个User列表和一个Article列表。Childern/Child与References/Reference的区别在于:
1. 删除一个对象时将会级联地删除Child和Children(通过调用Entity.doDelete()),而以References/Reference方式引用的对象不会被删除。
2. 从Children/Child中添加或移除移除一个对象时,这个对象将被持久化或删除,而References/Reference与持久化无关。
或者说,References/Reference只是简单地表示引用关系,与对象持久化无关;而Childern/Child表示严格的父子关系,与对象持久化密切相关。
接着看User的建模:
public class User extends Entity { private StringField name; private StringField password; private StringField blogName; private Children<Article> articleList; // View Attributes ... // Methods ... }
可以看到,user包含了name,password,blogName三个字符串型的Field,以及一个Article的列表。并且,User与Article之间是严格的父子关系。
在这个简单的示例中,由于Blog的属性比较简单,只有blogName,因此直接合并到了User中;并且Blog中没有分类(Category),因此User直接与Article关联。
如果Blog的属性比较复杂,并且带有分类,则User的建模方式可能是这样的:
public class User extends Entity { private StringField name; private StringField password; private Child<Blog> blog; private Children<Category> categoryList; // View Attributes ... // Methods ... }
这里需要说明的是*List这种命名方式,如果调用Entity的内置的doDelete(),doAdd()等方法,将会以这种默认的命名方式中父类中寻找相关的集合,然后将自己从这个集合中移除(或增加到集合中)。如果不以这种方式命名,那么就需要自己编写这些方法了。
关于Child/Reference的延迟加载:当调用Child/Reference的get()方法时,所引用的对象才会真正地被加载,这也是一种性能上的优化。
最后是Article的建模:
public class Article extends Entity { private DateField published; private StringField title; private TextField summary; private TextField content; private Reference<User> user; // View Attributes ... // Methods ... }
这里需要特别说明的是TextField类型:TextField类型实际上继承了Child<Text>,Text是特殊的Entity,这是为了利用延迟加载的特性;在参数解析和视图解析中对TextField进行了特殊处理,以保证使用上与StringField相似,同时还可以在视图中使用类似"${content.html}"的方法,将内容转为Html进行渲染(不过这个方法还有待完善)。
2.Rest风格URL与参数传递
举个例子:URL:http://localhost/User/save/name/aaa/password/bbb/blogName/ccc 将会创建一个User对象,将name赋值为aaa,将password赋值为bbb,将blogName赋值为ccc,并调用该对象的save()方法。当然,这种通过URL进行赋值的方式也可能带来安全隐患,尤其是当需要对对象进行更新并持久化时。因此,OMToolkit引入了更新选项(UpdateOptions)对这种赋值进行限制,这将在后面谈到。
另外,如果URL中包含id,则对象会被自动获取(通常是从cache中clone一个对象);如果URL中包含parent的id,则parent也会被自动获取(否则认定parent为database),这在增删对象和显示特定parent的列表时非常有用。
3.Session,Cookie,Request
可以随意地在Entity中声明session和cookies并使用,方法如下:
public class User extends Entity { // Fields ... private Session session; private List<Cookie> cookies; // Other view attributes ... // Other methods ... public String checkLogin() throws Exception { name.set(Cookie.get(cookies, "name")); password.set(Cookie.get(cookies, "password")); // Other codes .. } public String login() throws Exception { // Other codes ... if (remember) { cookies.add(new Cookie("name", name.get())); cookies.add(new Cookie("password", password.get())); } // Other codes ... } public String logout() throws Exception { session.set("userId", null); session.set("role", null); cookies.add(new Cookie("name", "")); cookies.add(new Cookie("password", "")); return toView("/Home/login"); } // Annotations ... public String save() throws Exception { // Other codes ... session.set("role", "user"); session.set("userId", getId()); // Other codes .. } // Other methods ... }
事实上,在生成Entity时,OMToolkit将会自动检查Entity的属性,并对其中的session和cookies进行赋值(如果有的话)。
Request对象可以通过Entity.getRequest()获取,但不推荐,因为实际上通过Fields和View Attributes就可以接收和设置参数了。
4.权限控制与更新选项
使用注解进行权限控制,并标明更新选项,方法如下:
public class User extends Entity { @Role("user") @UpdateOptions(toUpdate = "blogName[20]") public String update() throws Exception { // Codes ... } }
其中,Role注解可以放置在类或方法上,而UpdateOptions只能放置在方法上。
Role的value为数组,@Role({"user", "admin"}) 会从session中取出key为"role"的对象(String类型,目前还不支持数组类型),看是否包含于允许列表中,若不包含,则转向登录页面。登录页面可以在配置文件(Cfg.cfg)中进行配置。
UpdateOptions实际上是对输入URL进行检查,避免不合法的赋值,有toUpdate和allowEmpty两个属性。不合法的URL将被重定向到登陆页面。另外,在带有UpdateOptions的方法中获取对象时会申请互斥的更新锁,这是出于数据一致性的考虑,但实际上有潜在的性能问题,也容易导致死锁,是后续版本中需要优化的一个重点。
5.事务控制与自动提交
可以使用getTransaction()获取对事务的控制,OMToolkit在正常执行完一次操作时也会自动提交(有异常则rollback),对于没有添加@UpdateOptions注解的方法,将不会进行持久化或删除数据。这时transaction实际上只用于cache本次事务中获取的对象。
示例如下:
@Role("user") @UpdateOptions(toUpdate = "blogName[20]") public String update() throws Exception { // Other Codes ... if (userId == null || (Long) userId != getId()) { getTransaction().rollback(); return toView("/Home/login"); } // Other Codes ... }
6.分页与排序
Children/References的toList()方法将会按照id降序排列(即新创建的对象会排在前面),PageUtil.run(References<T>,Request)方法提供分页功能,写法如下:
pagedArticles = PageUtil.run(articleList, getRequest());
该方法将自动按照URL中的pageIndex和pageSize进行分页和排序,排序的依据仍然是id降序。
如果需要自定义的排序,可以通过Children/References的values()方法获取Child/Reference的集合,并将一个自定义的Comparator、pageIndex、pageSize一同传入ArrayUtil的toList(...)方法中。当然这种方式比较繁琐,在后续版本(varsion 0.0.3)中计划加入更便捷的排序和过滤API,实现类似下面的写法:
articleList.where("title like 'OMTookit%'") .orderBy("published desc") .toList(pageIndex, pageSize);
7.CRUD
下面快速地浏览一下OMTookit中的CRUD(User类中的方法):
列出用户
@Role("admin") public String list() throws Exception { return super.list(); }
这里直接使用了Entity的list()方法,Entity.list()实现如下:
public String list() throws Exception { return toView(PageUtil.run(doList(), request)); }
获取用户(同时显示该用户的blog中的所有文章):
public String get() throws Exception { pagedArticles = PageUtil.run(articleList, getRequest()); return toView(); }
请求URL中包含了id,该对象将自动被获取,所以不需要编写获取对象的语句。
注册新用户
@UpdateOptions(toUpdate = { "name[20]", "password[20]", "blogName[20]" }) public String save() throws Exception { if (exists()) { error = cfg("exists") + '\n'; return toView("/Home/register"); } doSave(); session.set("role", "user"); session.set("userId", getId()); return toView("/Home/index"); }
首先检查用户是否存在,若存在则设置错误信息并回到注册页面;doSave()方法会先在parent中找到"userList",再将自身添加到到列表中。之后在提交事务时将自动保存被创建的用户,并更新parent(即Database)。之后是session的设置,表示注册后自动登录。
除去检查用户是否存在和session设置的代码,实际实现保存用户的只有doSave()这一句。
更新用户(只更新博客名称):
@Role("user") @UpdateOptions(toUpdate = "blogName[20]") public String update() throws Exception { Object userId = session.get("userId"); if (userId == null || (Long) userId != getId()) { getTransaction().rollback(); return toView("/Home/login"); } return action("edit"); }
先检查用户的id是否与被修改的用户的id相同(防止非法修改他人的博客名称),然后返回到编辑页面。
看起来好像没有更新语句,这是因为OMTookit将自动保存被更改过的Entity(对于带有@updateOptions注解的方法)。需要注意的是getTransaction().rollback()是必不可少的,否则对象仍然会被更新(因为对象被更改过了)。这是少数需要开发者手动控制事务的地方。看起来好像这种自动保存机制带来了麻烦,但在大多数情况下应该还是会方便开发者的。
删除用户
@Role("admin") @UpdateOptions public String delete() throws Exception { return super.delete(); }
删除一个Entity的同时会删除它的child和children。
Entity本身有默认的CRUD实现,当然,大多数情况下还是需要开发者覆盖这些实现。
需要注意的有三点:
1. 如果在URL中指定id,则相应的对象将被自动获取
2. 如果在URL中指定parent的id,则相应的parrent将被自动加载,否则认定parent为Database。
3. 对于带有UpdateOptions注解的方法,OMTookit将自动保存被更新过的对象。
如果所请求的方法不存在,将自动调用toView()方法。所以Article没有实现get()方法(Entity中也没有),因为实际上不需要。只要在URL中指定了id,对象就会被获取,然后调用toView()方法,渲染该Entity。
8.视图渲染
继承与包含
以views/Home/index.html为例:
#extends /Master/home #override header <a href="/User/get/id/${userId}/pageSize/10">我的博客</a> <a href="/User/edit/id/${userId}/pageSize/10">博客设置</a> <a href="/User/logout">注销登录</a> #override content #include /Article/list/action/index/pageIndex/${pageIndex}/pageSize/10
被继承的views/Master/home.html如下:
#extends /Master/master #override title 博客首页 #override content <div id="header"> #abstract header </div> <h1>Oh, My Simple Blog!</h1> #abstract content
可以看到,#extends表示继承,views/Home/index.html继承了views/Master/home.html,而views/Master/home.html又继承了views/Master/master.html。#override表示覆盖,将会替换被继承视图的#abstract部分。
#include表示包含,后面接上被包含的URL。
需要注意的是,继承是静态的,而包含是动态的;即继承只是简单的替换,而包含则会在内部重新模拟一次请求。
循环与分支
#loop var ...(block) #end
有两种形式:不带参数和带一个参数,不带参数指对当前Model进行遍历,带参数则表示先获取该属性,再进行遍历。
#if var ...(block) #end
带一个参数,该参数的toString()方法返回"true"则解析语法块中的内容,否则不解析。
变量解析
解析的顺序是Model的get方法 -> Model属性 -> request -> session。
如"${pageIndex}",会先查找当前Model的getPageIndex()方法,然后是pageIndex属性,接着是request.params().get("pageIndex"),最后是session.get("pageIndex") 。都为null则作为空字符串进行渲染。
点操作符
"." 解析顺序是 Model的get方法 -> Model属性
如"content.html" 会先找content对象的getHtml()方法,然后找名为“html”的属性。
9.配置文件
有两类配置文件,一类是Cfg.cfg,实际上主要配置的是Web Server的参数,包括端口、缓冲区大小、线程池的线程数等。
另一类是cfg文件夹下的文件,以Entity类的名称命名,如User.cfg,主要用于配置一些提示信息等;可以用Entity.cfg(String name, String... args)获取配置文件中的值。
10.打包发布
将Web应用打包为jar(eclipse导出时只需勾选src文件夹),放在空模板中(即OMToolkit_0_0_1_bin.rar,可以在Google Code项目主页下载), 注意根据打包文件的名称,修改Cfg.cfg中的jar属性,并将cfg、data、resources、views目录复制到空模板中,覆盖原来的文件夹。(0.0.3 版本中会增加Ant脚本进行打包)
实现思路
1.Web Server的实现
Server的实现主要位于包com.omc.server。主要借助了Java NIO和concurrent包中的线程池。
网络信息的读写是单线程的,即每次从每个连接中读写一部分数据,而请求的处理则借助线程池。每次读写的数据量和线程池的线程数可以在Cfg.cfg中进行配置。
Server类的核心代码:
public class Server { public void run() throws Exception { init(); Selector selector = openSelector(); while (true) { doSelect(selector); } } private Selector openSelector() throws Exception { Selector selector = Selector.open(); ServerSocketChannel server = ServerSocketChannel.open(); server.configureBlocking(false); SelectionKey key = server.register(selector, SelectionKey.OP_ACCEPT); key.attach(new Accepter(key)); server.socket().bind(new InetSocketAddress(Cfg.port())); return selector; } private void doSelect(Selector selector) throws Exception { selector.select(); Set<SelectionKey> selected = selector.selectedKeys(); Iterator<SelectionKey> it = selected.iterator(); while (it.hasNext()) { ((OMRunnable) it.next().attachment()).run(); } selected.clear(); } // Other methods ... }
在openSelector()方法中,将一个Accepter类的对象附加到了ServerSocketChannel的key上,当key的状态为acceptable时,将被selector检索到,从而调用Accept对象的run()方法,以处理接收到的SocketChannel,Accepter的核心代码如下:
public class Accepter implements OMRunnable { // Other codes ... public void run() throws Exception { SocketChannel socket = accept(); socket.configureBlocking(false); SelectionKey k = register(socket); k.attach(new Worker(k)); } private SocketChannel accept() throws Exception { return ((ServerSocketChannel) key.channel()).accept(); } private SelectionKey register(SocketChannel socket) throws Exception { return socket.register(key.selector(), SelectionKey.OP_READ); } }
将被接收到的SocketChannel也注册到selector上,这样一来,当SocketChannel的key的状态为readable时,也会被selector检索到,从而调用Worker的run()方法。
Woker的实现较复杂,包含了Reading、Processing、Writing三个内部类,分别表示读取、处理、写回三种状态。Reading类每次读取一部分数据,读取完毕后创建一个Processing对象,并提交到线程池中(同时key的interest被设置为0,表示不关心任何操作);线程池在有线程空闲的情况下启动Processing对象的处理过程,处理完成后,将状态改为Writing(同时key的interest被设置为SelectionKey.OP_WRITE,表示只关心写操作)。
2.Web Framework的实现
Web Framework的部分主要位于com.omc.web包,实现了分发任务的控制器(Controller)、事务控制类(Transaction)、视图类(View)等。
Controller类主要通过从URL中解析得到的信息,创建Entity对象,对该对象的属性进行赋值,并调用相应的方法处理请求,最后进行视图渲染。主要运用了Java的反射机制。
Controller核心代码如下:
public class Controller { // Other codes ... public byte[] run() throws Exception { if (!AccessChecker.isAllow(req)) { return toLoin(); } String res = doRun(); List<Cookie> cookies = getCookies(entity); return response(res, cookies).getBytes(); } public String doRun() throws Exception { Method method = req.method(); UpdateOptions op = method.getAnnotation(UpdateOptions.class); Transaction transaction = loadEntity(op); try { Object res = method.invoke(entity); transaction.commit(); return (String) res; } catch (Exception e) { transaction.rollback(); e.printStackTrace(); return ""; } } // Other codes ... }
先检查权限、更新选项等,一旦发现非法访问,则重定向至登录页面;如果通过检查,则先加载Entity,然后用反射调用相应的方法,执行过程中没有错误则提交事务,否则回滚事务。
View的核心代码如下:
public class View { // Other codes ... public static String render(Object model, String view, Request request) throws Exception { String content = read("views/" + view + ".html").replace("\r", ""); return new View(model, content, request).render(); } private View(Object model, String content, Request request) { this.model = model; this.content = content; this.req = request; } public String render() throws Exception { if (content.startsWith("#extends")) { content = doExtends(content); return new View(model, content, req).render(); } lines = StringUtil.split(content, '\n'); while (index < lines.size()) { readLine(); if (s.startsWith("#if")) { branch(); } else if (s.startsWith("#loop")) { loop(); } else { parseLine(); } } return result.toString(); } // Other codes ... }
可以看到,View的构造函数是私有的,只能通过调用静态方法View.render(...)来渲染视图。
首先检查"#extends",如果存在则对内容进行替换,然后重新构造View并调用render()方法。然后逐行解析,遇到"#if"则进行分支处理,遇到"#loop"则进行循环处理,否则进行一般的解析。
3.Object-Orinted Database的实现
OODB的部分主要位于com.omc.data包,主要实现类包括DataUtil和FieldUtil。例如,DataUtil.get(long id, boolean forUpdate)方法用于获取一个对象:
public static Entity get(long id, boolean forUpdate) throws Exception { if (forUpdate) { while (locks.contains(id)) {} locks.add(id); } Entity entity = cache.get(id); return entity == null ? loadEntity(id) : entity; }
loadEntity(long id)方法如下:
private static Entity loadEntity(long id) throws Exception { Meta meta = Meta.get(id); Entity entity = meta.getEntity(); loadFields(entity, meta); entity.setId(id); cache.add(entity); return FieldUtil.clone(entity); }
loadFields(Entity entity, Meta meta)方法如下:
private static void loadFields(Entity entity, Meta meta) throws Exception { long position = meta.getPosition(); int size = meta.getSize(); String content = FileUtil.read(DATA_FILE, position, size); parseFields(entity, content); }
下面是对一些要点的说明:
meta与data
数据文件包括data/meta和data/data两个部分,分别包含了元数据(id,对象数据在data文件中的位置,Entity的类型)和对象数据。
OMTookit启动时,加载所有的meta(这可能会导致内存占用过高,是一个需要优化的地方)。
读取对象时,先从meta中读取对象数据所在位置,然后解析对象数据,生成对象。
保存对象时,将对象按一个的规则序列化并保存到data文件中,同时在meta文件中记录数据。
cache
对于已经加载的数据,在一定时间(可以在Cfg.cfg中配置)内将存在于cache中,再次读取时只需要clone一份,而不需要从数据文件中读取。
更新锁
当读取的对象的目的是更新("forUpdate")时,将申请该对象的更新锁,这意味着将阻塞其他"forUpdate"方式的读取。这可能是一个潜在的性能瓶颈,是后续版本需要进行优化的一个地方。
版本计划
偶数版本(0.0.2,0.0.4...)均为Bug修复与重构,奇数版本增加新功能。
0.0.3 计划
1. API改进,力求实现更为便捷的API
2. 文件上传
3. 404、500、502等错误页面
4. 排序、筛选、全文搜索等功能
5. 日志、性能、异常: 可以使用动态代理。OMField 和 Entity 也可以用动态代理加以改造。
6. 视图引擎增强。
7. XML、JSON支持。
8. Ant打包脚本。
0.0.5计划
1. 数据存储性能优化(更新锁机制,多级Meta)
更新锁需要改进,避免过多的互斥。
meta全量加载可能导致性能问题。如果将meta本身也视为数据的话,实际上可以建立meta的meta,进而建立n层的meta,从而实现更小的加载量。
2. 数据导入、导出、迁移工具。
0.0.7计划
1. Server性能优化。
2. 多应用支持。目前一个Server只支持运行一个应用。
每个小版本的周期约为1.5个月,大版本的周期约为1年。
系列文章发布计划
1. OMSimpleBlog详细说明(3月17日)
从零开始搭建OMSimpleBlog,主要分为用户注册与登录,文章发布与编辑,后台数据管理三个步骤。
2. OMTookit Web Server 详细说明(3月19日)
从零开始搭建Server,分为Server、Acceptpr、Worker的实现;Session、Cookie的处理。
3. OMToolkit Web Framework 详细说明(3月21日)
在前一部分的基础上,开发Web Framework,包括Controller的实现,Transaction的实现,以及View的实现。
4. OMToolkit Object-Orinted Database 详细说明(3月24日)
在前一部分的基础上,开发数据存储部分,包括meta data、对象的持久化、对象的读取、cache、更新锁等。
5. 总结(3月26日)
- OMToolkit_0_0_1_src.rar (105.7 KB)
- 下载次数: 9
- OMToolkit_0_0_1_bin.rar (71.6 KB)
- 下载次数: 5
- OMSimpleBlog_0_0_1_src.rar (119.4 KB)
- 下载次数: 6
- OMSimpleBlog_0_0_1_bin.rar (114.5 KB)
- 下载次数: 5
评论
楼主真够有精力了,不过很支持下!
我下载下看看web server性能如何?
oodb这个,我觉得实用性太低。
现在只是玩具而已,确实不实用。
楼主真够有精力了,不过很支持下!
我下载下看看web server性能如何?
oodb这个,我觉得实用性太低。
这个注解的作用是说明该Entity不会被持久化,因此不需要加载parent等。
不会导致错误,但会稍稍影响性能。
Master/user.html中
${name}
最好写作
<p>${name}</p>
以免没有文章时和提示信息混在一行中。
现在更新锁是直接用
if (forUpdate) { while (locks.contains(id)) {} locks.add(id); }
进行检查的,jdk concurrent包中有一些现成的锁,
但更理想的是不使用锁,而对不同的更新进行merge。
发表评论
-
OMToolkit介绍(5): 总结
2011-03-22 19:13 1732OMToolkit介绍(5): 总结 ( ... -
OMToolkit介绍(4) :Object-Oriented Database 实现
2011-03-22 07:26 1173OMToolkit介绍(4) :Object-Oriented ... -
OMToolkit介绍(3) :Web Framework 实现
2011-03-20 21:17 1566OMToolkit介绍(3) :Web Framework 实 ... -
OMToolkit介绍(2) :Web Server 实现
2011-03-19 04:18 1622OMToolkit介绍(2) :Web Server 实现 ... -
OMTookit介绍(1) 简单示例:OMSimpleBlog
2011-03-17 07:50 1271OMTookit介绍(1) 简单示 ...
相关推荐
:green_apple: CSharpGL是纯Objective-Orinted OpenGL包装器,没有任何第三方支持。 它从OpenGL API和通用要求中抽象出概念(缓冲区,着色器,状态,矩阵,矢量,纹理,画布,场景,相机,光源,拾取,文本,GUI .....
基于java的贝儿米幼儿教育管理系统答辩PPT.pptx
本压缩包资源说明,你现在往下拉可以看到压缩包内容目录 我是批量上传的基于SpringBoot+Vue的项目,所以描述都一样;有源码有数据库脚本,系统都是测试过可运行的,看文件名即可区分项目~ |Java|SpringBoot|Vue|前后端分离| 开发语言:Java 框架:SpringBoot,Vue JDK版本:JDK1.8 数据库:MySQL 5.7+(推荐5.7,8.0也可以) 数据库工具:Navicat 开发软件: idea/eclipse(推荐idea) Maven包:Maven3.3.9+ 系统环境:Windows/Mac
基于java的消防物资存储系统答辩PPT.pptx
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
TA_lib库(whl轮子),直接pip install安装即可,下载即用,非常方便,各个python版本对应的都有。 使用方法: 1、下载下来解压; 2、确保有python环境,命令行进入终端,cd到whl存放的目录,直接输入pip install TA_lib-xxxx.whl就可以安装,等待安装成功,即可使用! 优点:无需C++环境编译,下载即用,方便
使用软件自带的basic脚本编辑制作的脚本 低版本软件无法输出Excel报告,可以通过脚本方式实现这一功能
基于java的就业信息管理系统答辩PPT.pptx
25法理学背诵逻辑.apk.1g
基于java的大学生校园兼职系统答辩PPT.pptx
做到代码,和分析的源数据
本压缩包资源说明,你现在往下拉可以看到压缩包内容目录 我是批量上传的基于SpringBoot+Vue的项目,所以描述都一样;有源码有数据库脚本,系统都是测试过可运行的,看文件名即可区分项目~ |Java|SpringBoot|Vue|前后端分离| 开发语言:Java 框架:SpringBoot,Vue JDK版本:JDK1.8 数据库:MySQL 5.7+(推荐5.7,8.0也可以) 数据库工具:Navicat 开发软件: idea/eclipse(推荐idea) Maven包:Maven3.3.9+ 系统环境:Windows/Mac
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse
适用于ensp已经入门人群的学习,有一定难度
基于java的数码论坛系统设计与实现答辩PPT.pptx
tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl
基于java的医院信管系统答辩PPT.pptx
项目经过测试均可完美运行! 环境说明: 开发语言:java jdk:jdk1.8 数据库:mysql 5.7+ 数据库工具:Navicat11+ 管理工具:maven 开发工具:idea/eclipse