CppExplore

一切像雾像雨又像风

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  29 随笔 :: 0 文章 :: 280 评论 :: 0 Trackbacks

作者:CppExplore 网址:http://www.cppblog.com/CppExplore/
log模块是一个小模块,却是每个系统必备的模块。优秀的系统一定会有优秀的log信息,也可以说全面到位的log信息在一定程度上决定了一个系统的健壮性。在linux上,log模块是跟踪程序运行,验证业务逻辑正确的唯一方法。
一、功能
一个优秀的log系统应该包含以下功能:
(1)支持打印到屏幕、文件、socket、syslog
(2)支持分级别打印
(3)支持分模块打印
(4)支持多线程
(5)支持文件转储:按时间、按大小。
二、使用原则
方便说明,这里定义8个log级别:

typedef enum{
    XLOG_CRITICAL
=0x1,
    XLOG_ERROR
=0x2,
    XLOG_WARNING
=0x4,
    XLOG_NOTICE
=0x8,
    XLOG_INFO
=0x10,
    XLOG_DEBUG
=0x20,
    XLOG_TRACE
=0x40,
    XLOG_ALL
=0x80
}
XLogLevel;
使用方式举例如下:
X_LOG(XLOG_NOTICE,"message[%s:%d]\n",char_value,int_value);

打印log的原则:
(1)重要的函数(业务处理)入口处打印XLOG_DEBUG级别log,打印出函数名称、入口参数。
(2)函数有多个执行支路,在异常退出函数的支路上,打印XLOG_WARNING级别log,表明退出原因。
(3)系统调用发生异常,甚至造成程序退出的地方,打印XLOG_ERROR级别log,表明发生该错误的文件、行数、错误号
(4)为所有的类对象实现dump方法,该方法打印该类中的所有私有属性信息,以XLOG_NOTICE级别打印,当类对象中含有自定义的类属性时,该类的dump中打印属性类信息可以直接调用属性类的dump。运行期间,可以给用户提供输入接口:telnet、交互式shell命令行、或者简单的getchar,根据用户要求,执行顶级类对象的dump,以运行期间查看系统内所有数据的内部信息。
(5)该系统与外在系统的交互信息,往来数据包使用XLOG_INFO级别打印。
(6)为了调试系统,更为细微的查看系统的运行状况而加入的log,使用XLOG_TRACE级别。
三、log模块框架
可选的开源log很多:ace_log/Log4c/log4c**/log4c++/Pantheios/Log4cplus。依据开源项目活跃程度、库本身的独立性以及参考文档的数量,个人选择的开源库是Log4cplus。个人测试,未发现该库(v1.02)有bug或者内存泄漏现象,大力推荐。网上有关该库的文档的也相当多,我就不再罗唆了。
下面是我的log框架,不依赖于具体的log模块,相信你会喜欢:

#ifndef _X_LOG_H_
#define _X_LOG_H_
#include 
<stdio.h>
#include 
<errno.h>

#ifdef USE_LOG4CPLUS
    #include 
<log4cplus/configurator.h>
    #include 
<string>
    
static log4cplus::Logger logger= log4cplus::Logger::getInstance("Log");
    
static void init_log(const std::string & path)
    
{
        log4cplus::PropertyConfigurator::doConfigure(path);   
    }

    #define XLOG_ALL        log4cplus::TRACE_LOG_LEVEL
    #define XLOG_TRACE        log4cplus::TRACE_LOG_LEVEL
    #define XLOG_DEBUG        log4cplus::DEBUG_LOG_LEVEL
    #define XLOG_INFO            log4cplus::INFO_LOG_LEVEL
    #define XLOG_NOTICE        log4cplus::INFO_LOG_LEVEL
    #define XLOG_WARNING    log4cplus::WARN_LOG_LEVEL
    #define XLOG_ERROR        log4cplus::ERROR_LOG_LEVEL
    #define XLOG_CRITICAL    log4cplus::FATAL_LOG_LEVEL

    #define X_LOG(l, ) \
    
do { \
        
if(logger.isEnabledFor(l)) { \
            
char __buf__internal__[2046]={0}; \
            snprintf(__buf__internal__,
2045,__VA_ARGS__); \
            logger.forcedLog(l, __buf__internal__, __FILE__, 

__LINE__); \
        }
 \
    }
 while(0);
#elif define USE_ACE_LOG
    #include 
"ace/Log_Msg.h"
    #include 
"ace/Trace.h"
    #define XLOG_ALL        LM_TRACE
    #define XLOG_TRACE        LM_TRACE
    #define XLOG_DEBUG        LM_DEBUG
    #define XLOG_INFO             LM_INFO
    #define XLOG_NOTICE        LM_NOTICE
    #define XLOG_WARNING    LM_WARNING
    #define XLOG_ERROR        LM_ERROR
    #define XLOG_CRITICAL        LM_CRITICAL
    #define X_LOG(l,
do{ \
        ACE_DEBUG((l,
"[%T|%t] %s-%s:%

d
",__FILE__,__FUNCTION__,__LINE__)); \
        ACE_DEBUG((l,__VA_ARGS__)); \
        }
while(0)
#
else
    #include 
<pthread.h>
    #include 
<time.h>
    #include 
<sys/time.h>
    #define XLOG_LEVEL 
0xFF
    typedef 
enum{
        XLOG_CRITICAL
=0x1,
        XLOG_ERROR
=0x2,
        XLOG_WARNING
=0x4,
        XLOG_NOTICE
=0x8,
        XLOG_INFO
=0x10,
        XLOG_DEBUG
=0x20,
        XLOG_TRACE
=0x40,
        XLOG_ALL
=0x80
    }
XLogLevel;
    #define X_LOG(l,
do{ \
        
if(XLOG_LEVEL&l){ \
                struct timeval now;\
                gettimeofday(
&now,0); \
                struct tm 
*ptm=localtime(&(now.tv_sec)); \
                printf(
"[%d|%d:%d:%d.%d] [%s/%s/%d] 

",pthread_self(),ptm->tm_hour,ptm->tm_min,ptm-

>tm_sec,now.tv_usec,__FILE__,__FUNCTION__,__LINE__); \
                printf( __VA_ARGS__); \
            }
 \
        }
while(0)
#endif

#define die(str) 
{X_LOG(XLOG_WARNING,str); return;}

#define die_0(str) 
{X_LOG(XLOG_WARNING,str); return 0; }

#define die_1(str) 
{X_LOG(XLOG_WARNING,str); return -1; }

#define die_ns(str) 
{X_LOG(XLOG_WARNING,str); return ""; }

/*safe func return empty,0,-1*/
#define SAFE_FUNC(func) 
if((func)<0) \
    
{ \
        X_LOG(XLOG_ERROR,
"[%s:%d]error!error[%d:%s]

\n
",__FILE__,__LINE__,errno,strerror(errno)); \
        exit(-1); \
    }


/*safe func but 1 err return empty,0,-1*/
#define SAFE_FUNC_BUT_ERR1(func,ERR1) 
do \
    
{ \
        
if((func)<0){ \
            X_LOG(XLOG_ERROR,
"[%s:%d]error!error[%d:%s]

\n
",__FILE__,__LINE__,errno,strerror(errno)); \
            if(errno!=ERR1) exit(-1); \
        }
 \
        
else break; \
    }
while(1)

/*safe func but 2 err return empty,0,-1*/
#define SAFE_FUNC_BUT_ERR2(func,ERR1,ERR2) 
do \
    
{ \
        
if((func)<0){ \
            X_LOG(XLOG_ERROR,
"[%s:%d]error!error[%d:%s]

\n
",__FILE__,__LINE__,errno,strerror(errno)); \
            if(errno!=ERR1&&errno!=ERR2)  exit(-1); \
        }
 \
        
else break; \
    }
while(1)
#endif

当前的XLog.h文件实现了使用log4cplus、ace、printf三种方式,当然可以随意扩展。die则是XLOG_WARNING的打印方式,SAFE_FUNC是XLOG_ERROR的打印方式。
如果要使用log4cplus,请定义USE_LOG4CPLUS宏,使用的时候在进程开始处,

#ifdef  USE_LOG4CPLUS
  init_log(
"log.properties");
#endif

以X_LOG的方式使用log4cplus,你可以对log4cplus一无所知。执行你编译好的程序前,在可执行程序的目录下建立log.properties文件,内容你可以这样写:

# Define the root logger
log4cplus.rootLogger
=TRACE, consoleAppender, fileAppender

# Define a file appender named 
"consoleAppender"
log4cplus.appender.consoleAppender
=log4cplus::ConsoleAppender
log4cplus.appender.consoleAppender.layout
=log4cplus::PatternLayout
log4cplus.appender.consoleAppender.layout.ConversionPattern
=%-5p-[%t][%D{%H:%M:%%Q}]%m

# Define a file appender named 
"fileAppender"
log4cplus.appender.fileAppender
=log4cplus::RollingFileAppender
log4cplus.appender.fileAppender.MaxFileSize
=200KB
log4cplus.appender.fileAppender.File
=./log.log
log4cplus.appender.fileAppender.MaxBackupIndex
=3
log4cplus.appender.fileAppender.layout
=log4cplus::PatternLayout
log4cplus.appender.fileAppender.layout.ConversionPattern
=%-5p-[%t][%D{%H:%M:%%Q}]%m

有关log.properties文件的配置,可以自行去查找有关log4cplus的文章。如果你没使用过log4cplus,ok,那么下载一个log4cplus,编译,依据本文的XLog.h文件构建一个系统,尝试以下,你一定会惊叹log4cplus的强大与美妙。

posted on 2008-06-05 09:54 cppexplore 阅读(6753) 评论(23)  编辑 收藏 引用

评论

# re: 【原创】系统设计之 必备外围功能-log 2008-06-05 11:38 true
知识面很广啊,不知工作中开发什么呢  回复  更多评论
  

# re: 【原创】系统设计之 必备外围功能-log[未登录] 2008-06-05 12:08 cppexplore
@true
呵呵 linux上的应用服务器 软交换服务器 流媒体服务器等。  回复  更多评论
  

# re: 【原创】系统设计之 必备外围功能-log 2008-06-05 12:28 true
汗。。。,我和你可能有些缘分,我也在linux开发应用服务器,语音板卡,信令。哈哈。  回复  更多评论
  

# re: 【原创】系统设计之 必备外围功能-log[未登录] 2008-06-05 12:48 cppexplore
还没接触过语音板卡 我们的应用没有媒体编解码或者dtmf检测的。有时间向你讨教讨教,留个联系方式吧,我的gtalk/gmail是gamil.com的,msn是live.com的,名称都是zongjinliang。  回复  更多评论
  

# re: 【原创】系统设计之 必备外围功能-log 2008-06-05 17:50 Kevin Lynx
日志对于一个系统来说确实是必不可少的基础模块。
我用模板写了个不依赖于具体‘写’的日志类,简单的日志等级过滤。
///
/// @file kl_logger.h
/// @author Kevin Lynx
/// @date 4.21.2008
///
#ifndef ___KL_LOGGER_H_
#define ___KL_LOGGER_H_

#include <assert.h>
#include <stdarg.h>
#include <time.h>
#include <fstream>
#include "kl_compiler_cfg.h"

KL_COMMON_NAMESPACE_BEGIN

/// log level
enum log_level
{
LL_MIN = -1,
LL_ERROR,
LL_WARNING,
LL_INFO,
LL_DEBUG,
LL_MAX
};

///
/// default log pre-string, add system time and log level.
///
struct default_log_prestr
{
/// output pre-string in the buffer
/// @return the offset of the buf.
std::size_t operator() ( char *buf, int level )
{
char time[9];
char date[9];
_strtime( time );
_strdate( date );

const char *ll_desc = 0;
switch( level )
{
case LL_ERROR:
ll_desc = "ERROR";
break;
case LL_WARNING:
ll_desc = "WARNING";
break;
case LL_INFO:
ll_desc = "INFO";
break;
case LL_DEBUG:
ll_desc = "DEBUG";
break;

default:
ll_desc = "UNKNOWN";
}

// combine
sprintf( buf, "%s %s %s : ", date, time, ll_desc );
return strlen( buf ) ;
}
};

///
///
/// A simple logger class to write log information.
///
/// @param _Output where the log information to put, it must implement 'log( const char*)' function.
/// @param _PreStr used to write the pre-string of the log text like : 4.21.2008 : something.
/// @param string_size used when format string.(static buffer is more fast than dynamic buffer)
template <typename _Output, typename _PreStr = default_log_prestr, std::size_t _string_size = 1024>
class logger
{
public:
/// output object type
typedef _Output output_type;

/// pre-string type
typedef _PreStr prestr_type;

/// string size used when formatting strings.
enum
{
string_size = _string_size
};

public:
/// constructor
logger() :
_output( 0 ), _level( LL_DEBUG )
{
}

/// destructor
~logger()
{
}

/// set the output manager, you must call this function before you
/// log anything.
void set_output( output_type *ot )
{
assert( ot != 0 && "logger::set_output : invalid arguments." );
_output = ot;
}

/// write log text
void write_only( int level, const char *format, ... )
{
assert( _output != 0 && "logger::write_only : You must set up the output manager before you log anything." );
static char buf[string_size];

// checck the level
if( level > _level ) return ;

// format the string
va_list list;
va_start( list, format );
vsprintf( buf, format, list );
va_end( list );

// output the log text
_output->log( buf );
}

/// write log text and append prestring
void write( int level, const char *format, ... )
{
assert( _output != 0 && "logger::write : You must set up the output manager before you log anything." );
static char buf[string_size];

// checck the level
if( level > _level ) return ;

// append pre-string
std::size_t pos = _prestr( buf, level );

// format the string
va_list list;
va_start( list, format );
vsprintf( &buf[pos], format, list );
va_end( list );

// output the log text
_output->log( buf );
}

/// set log level
void set_level( int level )
{
assert( level > LL_MIN && level < LL_MAX && "logger::set_level : invalid arguments." );
_level = level;
}

/// get the log level
int get_level()
{
return _level;
}

private:
/// output manager to collect log text.
output_type *_output;
/// pre-str to append some text before the log text
prestr_type _prestr;
/// log level
int _level;
};

///
/// The file log output manager, write log text to the file.
///
class file_output
{
public:
/// constructor
file_output()
{
}

/// this constructor will open the file
file_output( const std::string filename, std::ios_base::openmode _Mode = std::ios_base::out ) :
_file_handle( filename.c_str(), _Mode )
{
}

/// destructor
~file_output()
{
}

/// open the file if it's not open
bool open( const std::string filename, std::ios_base::openmode _Mode = std::ios_base::out )
{
if( _file_handle.is_open() )
{
return false;
}

_file_handle.open( filename.c_str(), _Mode );
return _file_handle.is_open();
}

/// log
void log( const char *str )
{
assert( _file_handle.is_open() && "file_output::log : you cannot write anything before you open the file!" );
_file_handle << str ;
_file_handle.flush();
}

private:
/// output file
std::ofstream _file_handle;
};

KL_COMMON_NAMESPACE_END

#endif // end ___KL_LOGGER_H_  回复  更多评论
  

# re: 【原创】系统设计之 必备外围功能-log 2008-06-05 19:44 cppexplore
呵呵 以为我的log框架你会喜欢,看来我错了。不过还有一句话“如果你没使用过log4cplus,ok,那么下载一个log4cplus,编译,......,尝试以下,你一定会惊叹log4cplus的强大与美妙。”  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-02 13:18 mono
logger如果建立多个,日志文件会狂长,因为每次调用LOG4CPLUS_LEVEL,每个logger都记录一次

我奇怪,logger.shutdown 好像没有作用

看起来static 好像可以,但是如果你在多个地方都指定properties,logger还是会运行多次,而
log4cplus::PropertyConfigurator::doConfigure(LOG4CPLUS("log4cplus.properties"));
又不能static

当然导入一次properties会记录正确,我又不明白,如果你在某一个函数里声明,它什么时候会消亡呢。我觉得在某一个函数里声明properties,它就是全局的吗,那我希望某个地方换一个properties,比如某个函数是在不同的路径甚至机器下运行,还能不能保证还是这个properties,或者根据当前位置的相对路径无法找到需要的properties



另外,lz的宏编译错误,我自己写了一个宏

static log4cplus:tostringstream _str_buf;
static log4cplus::Logger logger= log4cplus::Logger::getInstance("filelogger");

X_LOG( logInfo, logLevel) \
_str_buf.clear();
_str_buf << logInfo;
logger.forceLog( logLevel, _str_buf.str(), __FILE__, __LINE__ )

在vs 2005下编译成功,并且也确实记录了,问题就是,即使你指定properties,它也不按照格式配置记录,而是自己把日志内容按行纪录,根本无视 时间、线程等等格式

这个宏与log4cplus本身的宏并没有什么区别,为什么看起来并没有读properties呢

  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-02 19:05 cppexplore
@mono
#define USE_LOG4CPLUS
之后就编译正确了吧,或者你把define USE_ACE_LOG那段去掉,可以用printf调式。
你的需求是什么样子的?我用于server端程序,在main函数开始,来句:
#ifdef USE_LOG4CPLUS
init_log("./log.properties");
#endif
之后就再就是正常的程序启动了,起各个协议栈、线程。当然log也都是打印在一个log文件里。
server端程序是一直运行的,不会退出,也就不存在消亡的问题。
  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-10 11:34 guosha
very good,
博主用过log4c吗?感觉如何?  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-10 11:56 cppexplore
@guosha
没有用过。
log是个小功能,封装下能做到透明使用基本就可以了。log4c依赖apr库,如果你的主体程序不是在apr上构建的话,引入log4c不太舒服,呵呵。  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-10 12:01 cppexplore
@mono,给你个可以编译通过的宏,不知道你是win下还linux下,下面的宏都可以,另外流式的log不容易和printf、ace等对应,也看个人喜好,我是认为printf的方式更容易格式化输出。
#define USE_LOG4CPLUS
#ifdef USE_LOG4CPLUS
#include <log4cplus/configurator.h>
#include <string>
static log4cplus::Logger logger= log4cplus::Logger::getInstance("Log");
static void init_log(const std::string & path)
{
log4cplus::PropertyConfigurator::doConfigure(path);
}
#define XLOG_ALL log4cplus::TRACE_LOG_LEVEL
#define XLOG_TRACE log4cplus::TRACE_LOG_LEVEL
#define XLOG_DEBUG log4cplus::DEBUG_LOG_LEVEL
#define XLOG_INFO log4cplus::INFO_LOG_LEVEL
#define XLOG_NOTICE log4cplus::INFO_LOG_LEVEL
#define XLOG_WARNING log4cplus::WARN_LOG_LEVEL
#define XLOG_ERROR log4cplus::ERROR_LOG_LEVEL
#define XLOG_CRITICAL log4cplus::FATAL_LOG_LEVEL

#ifdef WIN32
#if (_MSC_VER < 1500)
#define vsnprintf _vsnprintf
#endif
void inline win_log( int l, const char *format, ... )
{
if(logger.isEnabledFor(l)) {
schar buf[2046]={0};
va_list list;
va_start( list, format );
vsnprintf( buf, (size_t)2045,format, list );
va_end( list );
logger.forcedLog(l, buf, __FILE__, __LINE__);
}
}
#define X_LOG win_log
#else
#define X_LOG(l, ...) \
do { \
if(logger.isEnabledFor(l)) { \
char __buf__internal__[2046]={0}; \
snprintf(__buf__internal__,2045,__VA_ARGS__); \
logger.forcedLog(l, __buf__internal__, __FILE__, __LINE__); \
} \
} while(0)
#endif  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-17 21:55 田伯光
楼主,多进程可以用吗?  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-17 22:16 田伯光
楼主, 是把log4cplus编译成一个静态库或是动态库后就直接使用你这个套上去就可以用吗?  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-17 22:35 田伯光
看上去我好像已经弄明白怎么用了, 不知道博主可有比较完备一点的有关LOG配置的文档? 另外多进程程序使用这个log系统会不会有什么问题?  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-18 18:54 cppexplore
@田伯光
多个进程指向同一个文件的话,的确都可以写进去。至于是不是稳定就不清楚了,呵呵,我一直是单进程多线程下使用,偶尔忘记程序已经启动,第二次启动,同样会把log写进文件。
你多测试下吧。
log4cplus的配置文件的配置方式网上不少,或者你留个邮箱,我发给你。  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-23 15:30 田伯光
@cppexplore
guosha167@yahoo.com.cn

谢谢  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-23 16:54 田伯光
多进程的话,不知道它是不是可以做到在一个进程内改了配置,自动的其它进程也可以使用更改过后的配置,并且不需要重新启动程序。最好能做到交互式的,在程序运行期间能动态控制Loger的打印行为。  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-23 21:18 cppexplore
@田伯光
呵呵,多进程一定不能共享对象,除非这个对象在共享内存中,共享内存中的对象又要注意互斥问题。最好的办法还是各进程用自己的进程的log对象。运行期间动态改变log的级别,可以去代码中找答案,或者你等我有了这个需求,我改好告诉你,呵呵。  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2008-12-23 23:24 田伯光
@cppexplore
是这样的, 我以前写的简单的Log模块就是用共享内存把log的管理结构共享出来, 这样动态控制只需要更改这个结构的内容就可以了,但我以前写的只是控制一下级别跟打印的目的地,没有这个功能那么多,呵呵. C++我不熟悉, 所以不想去研究这个的代码,故问你一下以得到现成结果.
  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2008-12-24 09:20 cppexplore
@田伯光
log4cplus是线程安全的。多进程共享内存的方式使用对象,和多线程的方式是一样的。你可以压力下测试下,呵呵。
另外,打印到syslogd和打印到socket也是多进程打印log的备选方案。
log系统用开源也不是因为它们功能强大,主要的好处是它们充分考虑了IO输出的效率(开辟内存池,延迟批量输出),第二个是系统宕掉时候,log打印的准确性。另外用宏隔离,也方便以后发现更好log系统时候替换。  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log 2009-02-08 18:07 sashion
能否介绍一下log4cplus如何支持文件转储?能在core dump 之前把所有的对象都dump到日志中吗?是如何实现的?  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2009-02-19 10:36 cppexplore
@sashion
不晓的啊,可以去看源代码 :)  回复  更多评论
  

# re: 【原创】技术系列之 必备外围功能-log[未登录] 2011-06-04 13:31 cppexplore
2年前对日志完善了不少,以前的错误引导更正下
@田伯光
多进程打印问题:
不可以多进程打印, 除了写不能保证原子性,还有文件更换时的问题.
共享内存共享log对象可以, 这样还不如各自打印独立文件, 同时使用远程syslog方式统一打印到远程(需要自己实现appender)

动态生效问题:
可使用ConfigureAndWatchThread类达到目的, 同时log4cplus中有不少bug,使用这个功能时 要修改不少地方.

@sashion
上上个我的留言里说错了很多地方.
log4cplus没有提供延迟输出的策略, 新的维护者有计划增加异步,还没增加.实现异步延迟打印,需要自己进一步封装实现.

"能在core dump 之前把所有的对象都dump到日志中吗?"
不能. 因log4cplus没有提供延迟输出的策略, 日志同步打印,也不存在这个问题. 若自己实现异步延迟输出的话, 延迟输出的部分可以使用共享内存,另可捕获信号做一定补偿.  回复  更多评论
  


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理