设为首页 收藏本站
查看: 617|回复: 0

[经验分享] Apache Cassandra Learning Step by Step (5): 实战性的JTwissandra项目

[复制链接]

尚未签到

发表于 2017-1-14 09:44:29 | 显示全部楼层 |阅读模式
  在完成了Apache Cassandra的四个基本学习步骤之后,可以尝试下实战性的编码了。
  如有必要,建议再简单回顾一下:

Apache Cassandra Learning Step by Step (1)

Apache Cassandra Learning Step by Step (2): Core Concepts

Apache Cassandra Learning Step by Step (3): Samples ABC

Apache Cassandra Learning Step by Step (4): Data Modeling
  基于第四点的建模思路,接下来我们要做的,就是搭建一个叫做JTwissandra的实战性项目,就是所谓的Java版本的Twissandra了。
  其目的是为了以Twitter为假想对象,使用最简约(或者直接说简陋得了)的建模和实现,表达采用Apache Cassandra作为NoSQL平台的基本实现过程。
  JTwissandra的基本编码环境:
  1. Maven来管理
  2. JUnit来测试
  3. 基于hector client来作为Apache Cassandra的Java 客户端
  大家可以通过下面的Github链接,直接clone出来最新的代码:
  JTwissandra: https://github.com/itstarting/jtwissandra
  也欢迎大家Fork或在这里直接拍砖——反正咱在NoSQL也是新手,脸皮厚点不要紧啦:)
  1. 首先需要一个HFactoryHelper来初始化并建立Cassandra的客户端连接池和必要的对象:

import java.io.IOException;
import java.util.Properties;
import me.prettyprint.cassandra.model.ConfigurableConsistencyLevel;
import me.prettyprint.hector.api.Cluster;
import me.prettyprint.hector.api.HConsistencyLevel;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.factory.HFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper for Cassandra initialization
*
* @author bright_zheng
*
*/
public class HFactoryHelper {
private static Logger logger = LoggerFactory.getLogger(HFactoryHelper.class);
private static Cluster cluster;
private static Keyspace keyspace = initKeyspace();
private static Properties properties;
private HFactoryHelper(){}
public static Keyspace getKeyspace(){
return keyspace;
}
private static Keyspace initKeyspace() {
properties = new Properties();
try {
properties.load(HFactoryHelper.class.getResourceAsStream("/config.properties"));
} catch (IOException ioe) {
ioe.printStackTrace();
}
String cluster_name = properties.getProperty("cluster.name", "Test Cluster");
logger.debug("cluster.name={}", cluster_name);
String cluster_hosts = properties.getProperty("cluster.hosts", "127.0.0.1:9160");
logger.debug("cluster.hosts={}", cluster_hosts);
String active_keyspace = properties.getProperty("keyspace", "JTWISSANDRA");
logger.debug("keyspace={}", active_keyspace);
cluster = HFactory.getOrCreateCluster(cluster_name, cluster_hosts);
ConfigurableConsistencyLevel ccl = new ConfigurableConsistencyLevel();
ccl.setDefaultReadConsistencyLevel(HConsistencyLevel.ONE);
return HFactory.createKeyspace(
active_keyspace,
cluster,
ccl);
}
}
  2. 建立各项业务服务的基类BaseService。
  import java.util.UUID;

import me.prettyprint.cassandra.serializers.LongSerializer;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.cassandra.service.clock.MicrosecondsClockResolution;
import me.prettyprint.cassandra.utils.TimeUUIDUtils;
import me.prettyprint.hector.api.ClockResolution;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.beans.HColumn;
import me.prettyprint.hector.api.factory.HFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import bright.zheng.jtwissandra.HFactoryHelper;
/**
* Base service which all business services should extend
*
* @author bright_zheng
*
*/
public class BaseService {
protected Logger logger = LoggerFactory.getLogger(getClass());
protected static Keyspace KEYSPACE = HFactoryHelper.getKeyspace();
protected static final String CF_USER = "USER";
protected static final String CF_FRIEND = "FRIEND";
protected static final String CF_FOLLOWER = "FOLLOWER";
protected static final String CF_TWEET = "TWEET";
protected static final String CF_TIMELINE = "TIMELINE";
protected static final StringSerializer SERIALIZER_STRING
= StringSerializer.get();
protected static final LongSerializer SERIALIZER_LONG
= LongSerializer.get();
protected static final int TWEETS_LIMIT_DEFAULT = 10;
protected static final int TWEETS_LIMIT_MAX = 50;
protected HColumn<String, String> createColumn(String name, String value) {
return HFactory.createColumn(name, value, SERIALIZER_STRING, SERIALIZER_STRING);
}
protected HColumn<String, Long> createColumn(String name, Long value) {
return HFactory.createColumn(name, value, SERIALIZER_STRING, SERIALIZER_LONG);
}
protected HColumn<Long, String> createColumn(Long name, String value) {
return HFactory.createColumn(name, value, SERIALIZER_LONG, SERIALIZER_STRING);
}
/**
* REF: http://wiki.apache.org/cassandra/FAQ#working_with_timeuuid_in_java
*
* @return UUID
*/
public UUID getUUID(){
//TODO: which UUID should we use to make sure it's unique?
ClockResolution clock = new MicrosecondsClockResolution();
return TimeUUIDUtils.getTimeUUID(clock);
//return TimeUUIDUtils.getUniqueTimeUUIDinMillis();
}
protected Long getTimestamp(UUID uuid){
//return uuid.timestamp();
return TimeUUIDUtils.getTimeFromUUID(uuid);
}
protected Long generateTimestamp(){
return getTimestamp(getUUID());
}
}
   3. 下面是各项业务服务代码:
  3.1 UserService

package bright.zheng.jtwissandra.service;
import java.util.UUID;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.mutation.Mutator;
import bright.zheng.jtwissandra.bean.User;
/**
* User service
*
* @author bright_zheng
*
*/
public class UserService extends BaseService{
/**
* Sample CLI cmd:
* set USER['550e8400-e29b-41d4-a716-446655440000']['user_name'] = 'itstarting';
* set USER['550e8400-e29b-41d4-a716-446655440000']['password'] = '111222';
* set USER['550e8400-e29b-41d4-a716-446655440000']['create_timestamp'] = 1329836819890000;
*
* @param user
*/
public String addUser(User user) {
Mutator<String> mutator = HFactory.createMutator(
KEYSPACE, SERIALIZER_STRING);
UUID uuid = this.getUUID();
String user_uuid = uuid.toString();   
Long create_timestamp = this.getTimestamp(uuid);
logger.debug("user_uuid={}", user_uuid);  
logger.debug("user_name={}", user.getUser_name());
logger.debug("password={}", user.getUser_password());
logger.debug("create_timestamp={}", create_timestamp);
mutator.addInsertion(user_uuid, CF_USER,
this.createColumn("user_name", user.getUser_name()));
mutator.addInsertion(user_uuid, CF_USER,
this.createColumn("password", user.getUser_password()));
mutator.addInsertion(user_uuid, CF_USER,
this.createColumn("create_timestamp", create_timestamp));
mutator.execute();
//return the generated uuid
return user_uuid;
}   
}

   3.2 FriendService

package bright.zheng.jtwissandra.service;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.mutation.MutationResult;
import me.prettyprint.hector.api.mutation.Mutator;
/**
* Friend service
*
* @author bright_zheng
*
*/
public class FriendService extends BaseService{
/**
* Adding a friend has two business logic:
* 1. Add the friend's uuid to the Friend CF under my uuid
* 2. Add my uuid to the friend's uuid as follower
*
* set FRIEND['550e8400-e29b-41d4-a716-446655440000']['1329836819859000']
* = '550e8400-e29b-41d4-a716-446655440001;
*
* set FOLLOWER['550e8400-e29b-41d4-a716-446655440001']['1329836819859000'']
* = '550e8400-e29b-41d4-a716-446655440000;
*
* @param me
* @param friend
*/
public MutationResult followFriend(String me, String friend) {
Mutator<String> mutator = HFactory.createMutator(
KEYSPACE, SERIALIZER_STRING);
Long timestamp = this.generateTimestamp();
logger.debug("timestamp={}", timestamp);
mutator.addInsertion(me, CF_FRIEND,
this.createColumn(timestamp, friend));
mutator.addInsertion(friend, CF_FOLLOWER,
this.createColumn(timestamp, me));
return mutator.execute();
}   
}

   3.3 TimelineService

package bright.zheng.jtwissandra.service;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import me.prettyprint.hector.api.beans.ColumnSlice;
import me.prettyprint.hector.api.beans.HColumn;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.query.QueryResult;
import me.prettyprint.hector.api.query.SliceQuery;
import bright.zheng.jtwissandra.bean.Timeline;
/**
* Timeline service
*
* @author bright_zheng
*
*/
public class TimelineService extends BaseService{
/**
* get specified user's first Timeline
*
* @param user_uuid
* @return
*/
public TimelineWrapper getTimeline(String user_uuid){
return getTimeline(user_uuid, 0L, TWEETS_LIMIT_DEFAULT);
}
/**
* get specified user's Timeline with start point
*
* @param user_uuid
* @param start
* @return
*/
public TimelineWrapper getTimeline(String user_uuid, long start){
return getTimeline(user_uuid, start, TWEETS_LIMIT_DEFAULT);
}
/**
* get specified user's Timeline with start point and limit
*
* @param user_uuid
* @param start
* @param limit
* @return
*/
public TimelineWrapper getTimeline(String user_uuid, long start, int limit){
if (start<0) start = 0;
if (limit<0) limit = TWEETS_LIMIT_DEFAULT;
if (limit>TWEETS_LIMIT_MAX) limit = TWEETS_LIMIT_MAX;
SliceQuery<String, Long, String> sliceQuery =
HFactory.createSliceQuery(KEYSPACE, SERIALIZER_STRING, SERIALIZER_LONG, SERIALIZER_STRING);        
sliceQuery.setColumnFamily(CF_TIMELINE);
sliceQuery.setKey(user_uuid);
sliceQuery.setRange(start, Long.MAX_VALUE, false, limit+1);
QueryResult<ColumnSlice<Long, String>> result = sliceQuery.execute();
List<HColumn<Long, String>> list = result.get().getColumns();
long next = 0L;
if(list==null){
return new TimelineWrapper(null, next);
}else if (list.size()<=limit){        
return new TimelineWrapper(convertToTimeline(list), 0L);
}else{
HColumn<Long,String> last = list.get(list.size()-1);
next = last.getName(); //the name is the timestamp as the "next" start
list.remove(list.size()-1);
return new TimelineWrapper(convertToTimeline(list), next);
}
}
private List<Timeline> convertToTimeline(List<HColumn<Long,String>> cols){
Iterator<HColumn<Long,String>> it = cols.iterator();
List<Timeline> result = new ArrayList<Timeline>();
while(it.hasNext()){
HColumn<Long,String> col = it.next();
result.add(new Timeline(col.getValue(), col.getName()));
}
return result;
}
public class TimelineWrapper{
private List<Timeline> timelines;
private long nextTimeline;
public TimelineWrapper(List<Timeline> timelines, long nextTimeline){
this.timelines = timelines;
this.nextTimeline = nextTimeline;
}
public long getNextTimeline() {
return nextTimeline;
}
public List<Timeline> getTimelines() {
return timelines;
}
}
}

   3.4 TweetService

package bright.zheng.jtwissandra.service;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import me.prettyprint.hector.api.beans.ColumnSlice;
import me.prettyprint.hector.api.beans.HColumn;
import me.prettyprint.hector.api.beans.Row;
import me.prettyprint.hector.api.beans.Rows;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.mutation.Mutator;
import me.prettyprint.hector.api.query.MultigetSliceQuery;
import me.prettyprint.hector.api.query.QueryResult;
import me.prettyprint.hector.api.query.SliceQuery;
import bright.zheng.jtwissandra.bean.Tweet;
/**
* Tweet service
*
* @author bright_zheng
*
*/
public class TweetService extends BaseService{
/**
* Adding a tweet has following logic:
* 1. Save the tweet to CF of TWEET
* 2. Add the new tweet to my TIMELINE
* 3. Add the new tweet to all my followers' TIMELINE
*
* @param me
* @param friend
*/
public String addTweet(String user_uuid, String tweet_content) {
Mutator<String> mutator = HFactory.createMutator(
KEYSPACE, SERIALIZER_STRING);
//the tweet uuid
UUID uuid = this.getUUID();
String tweet_uuid = uuid.toString();
logger.debug("tweet_uuid={}", tweet_uuid);
//the timestamp to build the timeline
Long timestamp = this.getTimestamp(uuid);
logger.debug("timestamp={}", timestamp);
mutator.addInsertion(tweet_uuid, CF_TWEET,
this.createColumn("user_uuid", user_uuid));
mutator.addInsertion(tweet_uuid, CF_TWEET,
this.createColumn("tweet_content", tweet_content));
mutator.addInsertion(user_uuid, CF_TIMELINE,
this.createColumn(timestamp, tweet_uuid));
// get back all my follower and insert the tweet to his/her TIMELINE
SliceQuery<String, Long, String> sliceQuery =
HFactory.createSliceQuery(KEYSPACE, SERIALIZER_STRING, SERIALIZER_LONG, SERIALIZER_STRING);        
sliceQuery.setColumnFamily(CF_FOLLOWER);
sliceQuery.setKey(user_uuid);
sliceQuery.setRange(Long.MIN_VALUE, Long.MAX_VALUE, false, 500); //TODO: 500 followers hard code here?
QueryResult<ColumnSlice<Long, String>> result = sliceQuery.execute();
Iterator<HColumn<Long, String>> followers = result.get().getColumns().iterator();
while(followers.hasNext()) {
HColumn<Long, String> follower = followers.next();
String follower_uuid = follower.getValue();
logger.debug("follower's uuid={}", follower_uuid);
logger.debug("timestamp={}", follower.getName());
//insert the tweet to the follower's TIMELINE
mutator.addInsertion(follower_uuid, CF_TIMELINE,
this.createColumn(timestamp, tweet_uuid));
}
mutator.execute();
//return the new generated tweet's uuid
return tweet_uuid;
}
/**
* Should we add this service?
*
* @param tweet_uuid
* @return
*/
public Tweet getTweet(String tweet_uuid){
return null;
}
public List<Tweet> getTweets(List<String> tweet_uuids){
MultigetSliceQuery<String, String, String> multigetSlicesQuery =
HFactory.createMultigetSliceQuery(KEYSPACE, SERIALIZER_STRING, SERIALIZER_STRING, SERIALIZER_STRING);
multigetSlicesQuery.setColumnFamily(CF_TWEET);
multigetSlicesQuery.setColumnNames("user_uuid","tweet_content");        
multigetSlicesQuery.setKeys(tweet_uuids);
QueryResult<Rows<String, String, String>> results = multigetSlicesQuery.execute();
return convertRowsToTweets(results.get());
}
private List<Tweet> convertRowsToTweets(Rows<String, String, String> rows){
List<Tweet> list = new ArrayList<Tweet>();
Iterator<Row<String, String, String>> iterator = rows.iterator();
while(iterator.hasNext()){
Row<String, String, String> row = iterator.next();
ColumnSlice<String, String> cs = row.getColumnSlice();
list.add(new Tweet(row.getKey(),
cs.getColumnByName("tweet_content").getValue(),
cs.getColumnByName("user_uuid").getValue()));
}
return list;
}
}

   4. 当然少不了JUnit测试用例了:


package bright.zheng.jtwissandra;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import junit.framework.Assert;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import bright.zheng.jtwissandra.bean.Timeline;
import bright.zheng.jtwissandra.bean.Tweet;
import bright.zheng.jtwissandra.bean.User;
import bright.zheng.jtwissandra.service.FriendService;
import bright.zheng.jtwissandra.service.TimelineService;
import bright.zheng.jtwissandra.service.TimelineService.TimelineWrapper;
import bright.zheng.jtwissandra.service.TweetService;
import bright.zheng.jtwissandra.service.UserService;
/**
* Test cases for all services currently provided.
* Please drop and create schema first and then run all cases as one round
* The 'me' and 'friend' will be created each round dynamically for easier testing
*
* @author bright_zheng
*
*/
public class ServiceTest{
Logger logger = LoggerFactory.getLogger(ServiceTest.class);
private static UserService SERVICE_USER = new UserService();
private static FriendService SERVICE_FRIEND = new FriendService();
private static TweetService SERVICE_TWEET = new TweetService();
private static TimelineService SERVICE_TIMELINE = new TimelineService();
private static String me;
private static String friend;
private static long nextTimeline = 0L;
@BeforeClass
public static void setUp(){
//
}
@Test
public void addUser() {
logger.debug("=====================addUser{====================");
//add user 1
me = SERVICE_USER.addUser(new User("itstarting","1234"));
logger.debug("This round of tesing, ME={}", me);
Assert.assertNotNull(me);
//add user 2
friend = SERVICE_USER.addUser(new User("test1","1234"));
logger.debug("This round of tesing, FRIEND={}", friend);
Assert.assertNotNull(friend);
logger.debug("=====================}//addUser====================");
}   
/**
* I'm following a friend
*/
@Test
public void followFriend() {
logger.debug("=====================followFriend{====================");
SERVICE_FRIEND.followFriend(me, friend);
logger.debug("=====================}//followFriend====================");
}
/**
* I'm followed by a follower
*/
@Test
public void followedByFollower() {
logger.debug("=====================followedByFollower{====================");
SERVICE_FRIEND.followFriend(friend, me);
logger.debug("=====================}//followedByFollower====================");
}
/**
* I'm twittering
*/
@Test
public void addTweetByMe() {
logger.debug("=====================addTweetByMe{====================");
for(int i=0; i<100; i++){
String tweet_uuid = SERVICE_TWEET.addTweet(me, "Hellow JTWISSANDRA -- by itstarting:" + i);
Assert.assertNotNull(tweet_uuid);
}
logger.debug("=====================}//addTweetByMe====================");
}
/**
* My friend is twittering
*
*/
@Test
public void addTweetByFriend() {
logger.debug("=====================addTweetByFriend{====================");
for(int i=0; i<100; i++){
String tweet_uuid = SERVICE_TWEET.addTweet(friend, "Hellow JTWISSANDRA -- by test1:" + i);
Assert.assertNotNull(tweet_uuid);
}
logger.debug("=====================}//addTweetByFriend====================");
}
/**
* Get tweets for me
*/
@Test
public void getTweetsByMe(){
logger.debug("=====================getTweetsByMe{====================");
getTweets(me, 0);
logger.debug("=====================}//getTweetsByMe====================");
}
/**
* Get tweets at next Timeline (if any)
*/
@Test
public void getTweetsByMeForNextTimeline(){
logger.debug("=====================getTweetsByMeForNextTimeline{====================");
if(nextTimeline>0L){
getTweets(me, nextTimeline);
}
logger.debug("=====================}//getTweetsByMeForNextTimeline====================");
}
/**
* Get tweets for my friend
*/
@Test
public void getTweetsByMyFriend(){
logger.debug("=====================getTweetsByMyFriend{====================");
getTweets(friend, 0);
logger.debug("=====================}//getTweetsByMyFriend====================");
}
/**
*
*/
@Test
public void getTweetsByMyFriendForNextTimeline(){
logger.debug("=====================getTweetsByMyFriendForNextTimeline{====================");
getTweets(friend, nextTimeline);
logger.debug("=====================}//getTweetsByMyFriendForNextTimeline====================");
}
private void getTweets(String user_uuid, long start){
TimelineWrapper wrapper = SERVICE_TIMELINE.getTimeline(user_uuid, start);
Assert.assertNotNull(wrapper);
List<Timeline> list = wrapper.getTimelines();
List<String> tweet_uuids = new ArrayList<String>();
for(Timeline timeline: list){
String tweet_uuid = timeline.getTweet_uuid();
logger.debug("From Timeline: tweet_uuid={}, tweet_timestamp={}",
tweet_uuid, timeline.getTweet_timestamp());
tweet_uuids.add(tweet_uuid);
}
List<Tweet> tweets = SERVICE_TWEET.getTweets(tweet_uuids);
Iterator<Tweet> it = tweets.iterator();
while(it.hasNext()){
Tweet tweet = it.next();
logger.debug("From Tweet: tweet_uuid={}, tweet_content={}, user_uuid={}",
new Object[]{tweet.getTweet_uuid(),
tweet.getTweet_content(),
tweet.getUser_uuid()
});
}
if(wrapper.getNextTimeline() > 0L){
logger.debug("The start timeline of next page is: {}", wrapper.getNextTimeline());
nextTimeline = wrapper.getNextTimeline();
}else{
logger.debug("No next page available");
nextTimeline = 0L;
}
}
@AfterClass
public static void shutdown(){
//cluster.getConnectionManager().shutdown();
}
}

  这是个一锅端的测试用例,全部跑一次可以覆盖几乎所有的业务服务逻辑。
  5. 最后,别忘了在跑之前创建必要的schema:


drop keyspace JTWISSANDRA;
create keyspace JTWISSANDRA
with placement_strategy = 'org.apache.cassandra.locator.SimpleStrategy'
and strategy_options = [{replication_factor:1}];
use JTWISSANDRA;
create column family USER
with comparator = UTF8Type   
and key_validation_class = UTF8Type
and default_validation_class = UTF8Type
and column_metadata = [
{column_name: user_name, validation_class: UTF8Type,
index_name:user_name_idx, index_type:KEYS }
{column_name: user_password, validation_class: UTF8Type}
{column_name: create_timestamp, validation_class: LongType,
index_name:create_timestamp_idx, index_type:KEYS}
];
create column family FRIEND
with comparator = LongType   
and key_validation_class = UTF8Type
and default_validation_class = UTF8Type;
create column family FOLLOWER
with comparator = LongType   
and key_validation_class = UTF8Type
and default_validation_class = UTF8Type;
create column family TWEET
with comparator = UTF8Type   
and key_validation_class = UTF8Type
and default_validation_class = UTF8Type
and column_metadata = [
{column_name: user_uuid, validation_class: UTF8Type}
{column_name: tweet_content, validation_class: UTF8Type}
];
create column family TIMELINE
with comparator = LongType   
and key_validation_class = UTF8Type
and default_validation_class = UTF8Type;

  6. 其他?
  有些咋想,就不贴了吧,略过……有兴趣的可从Github clone下来跑跑看。
  ==========发现的问题,接下来再谈再讨论,希望有丰富经验的高手前来助阵,去我谜团,谢谢!==========

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-328200-1-1.html 上篇帖子: [Apache Click快速开发]基于Jquery Dialog的Click Dialog组件 下篇帖子: Cocos2d-html5 通过apache ant编译成web应用以便部署到服务器
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表