return;
} // @1
this.query( sql ); //@2
}
代码@1,从COM_QUERY命令报文中解析出具体的SQL查询语句
代码@2,根据SQL进行查询。
接下来重点关注query( sql )的实现:
public void query(String sql) {
if (sql == null || sql.length() == 0) {
writeErrMessage(ErrorCode.ER_NOT_ALLOWED_COMMAND, “Empty SQL”);
return;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(new StringBuilder().append(this).append(" ").append(sql).toString());
}
// remove last ‘;’
if (sql.endsWith(“;”)) {
sql = sql.substring(0, sql.length() - 1);
}
// 记录SQL
this.setExecuteSql(sql);
// 防火墙策略( SQL 黑名单/ 注入攻击) // @1 start
if ( !privileges.checkFirewallSQLPolicy( user, sql ) ) {
writeErrMessage(ErrorCode.ERR_WRONG_USED,
“The statement is unsafe SQL, reject for user '” + user + “'”);
return;
}
// DML 权限检查
try {
boolean isPassed = privileges.checkDmlPrivilege(user, schema, sql);
if ( !isPassed ) {
writeErrMessage(ErrorCode.ERR_WRONG_USED,
“The statement DML privilege check is not passed, reject for user '” + user + “'”);
return;
}
} catch( com.alibaba.druid.sql.parser.ParserException e1) {
writeErrMessage(ErrorCode.ERR_WRONG_USED, e1.getMessage());
LOGGER.error(“parse exception”, e1 );
return;
} // @1 end
// 执行查询
if (queryHandler != null) { // @2
queryHandler.setReadOnly(privileges.isReadOnly(user));
queryHandler.query(sql);
} else {
writeErrMessage(ErrorCode.ER_UNKNOWN_COM_ERROR, “Query unsupported!”);
}
}
该方法的实现,主要是先判断sql的合法性,然后判断该用户的执行权限,如果都没问题,交给QueryHandler去执行。
代码@1,用户权限检测,实现基于阿里开源的druid实现,在后续SQL解析专题会详细学习,目前不做详细解读。
代码@2,交给QueryHandler执行sql命令。
接下来将执行ServerQueryHandler.query方法,该方法,主要就是解析COM_QUERY的命令类型,比如SELECT语句亦或是USE语句等,本文以Select语句为例进行跟踪讲解,那select命令会被SelectHandler处理(工具类),最终会进入到ServerConnection的execute(String sql, int sqlType)
ServerConnection的execute(String sql, int sqlType)方法如下:
public void execute(String sql, int type) {
// 连接状态检查
if (this.isClosed()) {
LOGGER.warn("ignore execute ,server connection is closed " + this);
return;
}
// 事务状态检查
if (txInterrupted) {
writeErrMessage(ErrorCode.ER_YES, “Transaction error, need to rollback.” + txInterrputMsg);
return;
}
// 检查当前使用的DB //@1
String db = this.schema;
boolean isDefault = true;
if (db == null) {
db = SchemaUtil.detectDefaultDb(sql, type);
if (db == null) {
writeErrMessage(ErrorCode.ERR_BAD_LOGICDB, “No MyCAT Database selected”);
return;
}
isDefault = false;
}
// @2 start
// 兼容PhpAdmin’s, 支持对MySQL元数据的模拟返回
TODO: 2016/5/20 支持更多information_schema特性
if (ServerParse.SELECT == type && db.equalsIgnoreCase(“information_schema”)) {
MysqlInformationSchemaHandler.handle(sql, this);
return;
}
if (ServerParse.SELECT == type && sql.contains(“mysql”) && sql.contains(“proc”)) {
SchemaUtil.SchemaInfo schemaInfo = SchemaUtil.parseSchema(sql);
if (schemaInfo != null && “mysql”.equalsIgnoreCase(schemaInfo.schema)
&& “proc”.equalsIgnoreCase(schemaInfo.table)) {
// 兼容MySQLWorkbench
MysqlProcHandler.handle(sql, this);
return;
}
}
SchemaConfig schema = MycatServer.getInstance().getConfig().getSchemas().get(db);
if (schema == null) {
writeErrMessage(ErrorCode.ERR_BAD_LOGICDB, “Unknown MyCAT Database '” + db + “'”);
return;
}
// fix navicat SELECT STATE AS State
, ROUND(SUM(DURATION),7) AS
// Duration
, CONCAT(ROUND(SUM(DURATION)/*100,3), ‘%’) AS Percentage
// FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID= GROUP BY STATE
// ORDER BY SEQ
if (ServerParse.SELECT == type && sql.contains(" INFORMATION_SCHEMA.PROFILING ")
&& sql.contains(“CONCAT(ROUND(SUM(DURATION)/”)) {
InformationSchemaProfiling.response(this);
return;
} //@2 end
/*
-
当已经设置默认schema时,可以通过在sql中指定其它schema的方式执行 相关sql,已经在mysql客户端中验证。
-
所以在此处增加关于sql中指定Schema方式的支持。
*/
if (isDefault && schema.isCheckSQLSchema() && isNormalSql(type)) {
SchemaUtil.SchemaInfo schemaInfo = SchemaUtil.parseSchema(sql);
if (schemaInfo != null && schemaInfo.schema != null && !schemaInfo.schema.equals(db)) {
SchemaConfig schemaConfig = MycatServer.getInstance().getConfig().getSchemas().get(schemaInfo.schema);
if (schemaConfig != null)
schema = schemaConfig;
}
} //@3
routeEndExecuteSQL(sql, type, schema); //@4
}
本文不试图详细分析每个步骤的具体实现,故只描述上面代码段的作用,详细的分析会以专题的形式讲解,比如路由解析,Schema解析等。
代码@1,解析schema。
代码@2,兼容各个客户端的数据报文格式,基于抓包工具,了解各客户端与mysql服务端的交互协议,从而编写适应性代码。
代码@3,对schema标签checkSQLschema属性的处理逻辑。
代码@4,路由并执行。继续跟踪代码@4,以便继续探究前端连接与后端连接的交互。
继续进入到ServerConnection的routeEndExecuteSQL方法:
public void routeEndExecuteSQL(String sql, int type, SchemaConfig schema) {
// 路由计算
RouteResultset rrs = null;
try {
rrs = MycatServer.getInstance().getRouterservice().route(MycatServer.getInstance().getConfig().getSystem(),
schema, type, sql, this.charset, this); //@1
} catch (Exception e) {
StringBuilder s = new StringBuilder();
LOGGER.warn(s.append(this).append(sql).toString() + " err:" + e.toString(), e);
String msg = e.getMessage();
writeErrMessage(ErrorCode.ER_PARSE_ERROR, msg == null ? e.getClass().getSimpleName() : msg);
return;
}
if (rrs != null) {
// session执行
session.execute(rrs, rrs.isSelectForUpdate() ? ServerParse.UPDATE : type); //@2
}
}
计算路由,如果找到路由节点并执行着,从这里看出,接近我们的目标了。同样在该文中不会详细解读路由算法的计算,重在理解执行流程,这里将引出一个关键的对象session,见代码@2,那Session是何许人也呢?原来是NonBlockingSession对象。session初始化的地方在:
也就是每一个前端连接,持有一NonBlockingSession对象。我们先关注该类一个重要的属性:
private final ConcurrentHashMap<RouteResultsetNode, BackendConnection> target = new ConcurrentHashMap<RouteResultsetNode, BackendConnection>(2, 0.75f);
首先一个NonBlockingSession持有一个前端连接(FrontedConnection,ServerConnection),然后在持有后端连接上,以每个路由节点(分片节点,datanode)为键,存放一个后端连接BackendConnection。
那就继续浏览session.execute方法源码:
@Override
public void execute(RouteResultset rrs, int type) {
// clear prev execute resources
clearHandlesResources();
if (LOGGER.isDebugEnabled()) {
StringBuilder s = new StringBuilder();
LOGGER.debug(s.append(source).append(rrs).toString() + " rrs ");
}
// 检查路由结果是否为空
RouteResultsetNode[] nodes = rrs.getNodes(); // @1
if (nodes == null || nodes.length == 0 || nodes[0].getName() == null || nodes[0].getName().equals(“”)) {
source.writeErrMessage(ErrorCode.ER_NO_DB_ERROR,
“No dataNode found ,please check tables defined in schema:” + source.getSchema());
return;
}
boolean autocommit = source.isAutocommit();
final int initCount = target.size(); //@2
if (nodes.length == 1) { //@3
singleNodeHandler = new SingleNodeHandler(rrs, this);
if (this.isPrepared()) {
singleNodeHandler.setPrepared(true);
}
try {
if(initCount > 1){
checkDistriTransaxAndExecute(rrs,1,autocommit);
}else{
singleNodeHandler.execute(); //@4
}
} catch (Exception e) {
LOGGER.warn(new StringBuilder().append(source).append(rrs).toString(), e);
source.writeErrMessage(ErrorCode.ERR_HANDLE_DATA, e.toString());
}
} else {
multiNodeHandler = new MultiNodeQueryHandler(type, rrs, autocommit, this);
if (this.isPrepared()) {
multiNodeHandler.setPrepared(true);
}
try {
if(((type == ServerParse.DELETE || type == ServerParse.INSERT || type == ServerParse.UPDATE) && !rrs.isGlobalTable() && nodes.length > 1)||initCount > 1) {
checkDistriTransaxAndExecute(rrs,2,autocommit);
} else {
multiNodeHandler.execute();
}
} catch (Exception e) {
LOGGER.warn(new StringBuilder().append(source).append(rrs).toString(), e);
source.writeErrMessage(ErrorCode.ERR_HANDLE_DATA, e.toString());
}
}
if (this.isPrepared()) {
this.setPrepared(false);
}
}
我们以单节点路由信息为例来讲解与后端连接的关系,那代码的执行路劲为 @1 --》@2–》@3–》@4,进入到SingleNodeHandler的execute方法,该类是单节点的执行逻辑的抽象,持有路由分片信息RouteResultset与NonBlockingSession对象。
继续进入到SingleNodeHandler的execute方法中:
public void execute() throws Exception {
startTime=System.currentTimeMillis();
ServerConnection sc = session.getSource();
this.isRunning = true;
this.packetId = 0;
final BackendConnection conn = session.getTarget(node);
LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
node.setRunOnSlave(rrs.getRunOnSlave()); // 实现 master/slave注解
LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());
if (session.tryExistsCon(conn, node)) { //@1
_execute(conn);
} else { //@2
// create new connection
MycatConfig conf = MycatServer.getInstance().getConfig();
LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());
node.setRunOnSlave(rrs.getRunOnSlave()); // 实现 master/slave注解
LOGGER.debug("node.getRunOnSlave() " + node.getRunOnSlave());
PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
dn.getConnection(dn.getDatabase(), sc.isAutocommit(), node, this, node);
}
}
如果已经有后台连接了,就直接用后台连接执行,否则,从后端连接池中获取一个连接,这里将引出后端连接中一个重要的类:PhysicalDBNode,其getConnection为获取连接的核心实现。
如果有已经存在连接,执行SingleNodeHandler的_execute方法:
private void _execute(BackendConnection conn) {
if (session.closed()) {
endRunning();
session.clearResources(true);
return;
}
conn.setResponseHandler(this); //@1
try {
conn.execute(node, session.getSource(), session.getSource().isAutocommit()); //@2
} catch (Exception e1) {
executeException(conn, e1);
return;
}
}
代码@1,为后端连接设置ResponseHandler,在后端连接收到后端服务器的响应报文后,会交给该RersponseHandler,完成从后端连接到前端连接数据的返回。
代码@2,执行BackendConnection 的execute方法,完成与后端服务器的命令执行。
重点关注:BackendConnection的execute方法,该方法的职责肯定是按照mysql通信协议命令请求报文,发送到后端服务器。该方法有涉及到分布式事务的处理(XA事务的实现)
public void execute(RouteResultsetNode rrn, ServerConnection sc,
boolean autocommit) throws UnsupportedEncodingException {
if (!modifiedSQLExecuted && rrn.isModifySQL()) {
modifiedSQLExecuted = true;
}
String xaTXID = sc.getSession2().getXaTXID();
synAndDoExecute(xaTXID, rrn, sc.getCharsetIndex(), sc.getTxIsolation(),
autocommit);
}
private void synAndDoExecute(String xaTxID, RouteResultsetNode rrn,
int clientCharSetIndex, int clientTxIsoLation,
boolean clientAutoCommit) {
String xaCmd = null;
boolean conAutoComit = this.autocommit;
String conSchema = this.schema;
// never executed modify sql,so auto commit
boolean expectAutocommit = !modifiedSQLExecuted || isFromSlaveDB()
|| clientAutoCommit;
if (expectAutocommit == false && xaTxID != null && xaStatus == TxState.TX_INITIALIZE_STATE) {
//clientTxIsoLation = Isolations.SERIALIZABLE;
xaCmd = "XA START " + xaTxID + ‘;’;
this.xaStatus = TxState.TX_STARTED_STATE;
}
int schemaSyn = conSchema.equals(oldSchema) ? 0 : 1;
int charsetSyn = 0;
if (this.charsetIndex != clientCharSetIndex) {
//need to syn the charset of connection.
//set current connection charset to client charset.
//otherwise while sending commend to server the charset will not coincidence.
setCharset(CharsetUtil.getCharset(clientCharSetIndex));
charsetSyn = 1;
}
int txIsoLationSyn = (txIsolation == clientTxIsoLation) ? 0 : 1;
int autoCommitSyn = (conAutoComit == expectAutocommit) ? 0 : 1;
int synCount = schemaSyn + charsetSyn + txIsoLationSyn + autoCommitSyn;
if (synCount == 0 && this.xaStatus != TxState.TX_STARTED_STATE) {
// not need syn connection
sendQueryCmd(rrn.getStatement());
return;
}
CommandPacket schemaCmd = null;
StringBuilder sb = new StringBuilder();
if (schemaSyn == 1) {
schemaCmd = getChangeSchemaCommand(conSchema);
// getChangeSchemaCommand(sb, conSchema);
}
if (charsetSyn == 1) {
getCharsetCommand(sb, clientCharSetIndex);
}
if (txIsoLationSyn == 1) {
getTxIsolationCommand(sb, clientTxIsoLation);
}
if (autoCommitSyn == 1) {
getAutocommitCommand(sb, expectAutocommit);
}
if (xaCmd != null) {
sb.append(xaCmd);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("con need syn ,total syn cmd " + synCount
-
" commands " + sb.toString() + “schema change:”
-
(schemaCmd != null) + " con:" + this);
}
metaDataSyned = false;
statusSync = new StatusSync(xaCmd != null, conSchema,
clientCharSetIndex, clientTxIsoLation, expectAutocommit,
synCount);
// syn schema
if (schemaCmd != null) {
schemaCmd.write(this);
}
// and our query sql to multi command at last
sb.append(rrn.getStatement()+“;”);
// syn and execute others
this.sendQueryCmd(sb.toString());
// waiting syn result…
}
加上事务的处理等,最终执行this.sendQueryCmd,这一路走来,沿途有好多风景,后续会详细解读,比如schema解析,路由、后端连接获取、分布是XA事务等,然后进入到sendQueryCmd方法:
protected void sendQueryCmd(String query) {
CommandPacket packet = new CommandPacket();
packet.packetId = 0;
packet.command = MySQLPacket.COM_QUERY;
try {
packet.arg = query.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
lastTime = TimeUtil.currentTimeMillis();
packet.write(this);
}
最后将查询命令按照mysql通信协议发送到服务端。最终会调用后端连接的MysqlConnection的write方法,具体实现见:
@Override
public final void write(ByteBuffer buffer) {
if (isSupportCompress()) {
ByteBuffer newBuffer = CompressUtil.compressMysqlPacket(buffer, this, compressUnfinishedDataQueue);
writeQueue.offer(newBuffer);
} else {
writeQueue.offer(buffer);
}
// if ansyn write finishe event got lock before me ,then writing
// flag is set false but not start a write request
// so we check again
try {
this.socketWR.doNextWriteCheck();
} catch (Exception e) {
LOGGER.warn(“write err:”, e);
this.close(“write err:” + e);
}
}
到这里为止,就完成了一条客户端查询命令经过层层关卡到到了后端连接,并发送给了后端服务器。
那还剩一个问题,前端基于主从Reactor模型完成与客户端的请求处理,那mycat与后端mysql服务器是怎么处理请求响应的呢?
要明白这个问题,请看第三部分,后端连接建立已经IO线程模型。
3、后端连接建立以及IO线程模型
================
从上文建立连接的地方见SingelNodeHandler.execute方法中,如果连接未创建,则调用PhysicalDBNode的getConnection方法:
SingleNodeHandler.execute方法:
PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
dn.getConnection(dn.getDatabase(), sc.isAutocommit(), node, this, node);
3.1 源码分析PhysicalDBNode的getConnection方法
public void getConnection(String schema,boolean autoCommit, RouteResultsetNode rrs,
ResponseHandler handler, Object attachment) throws Exception { //@1
checkRequest(schema); //@2
if (dbPool.isInitSuccess()) {
LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
if(rrs.getRunOnSlave() != null){ // 带有 /db_type=master/slave/ 注解 //@3
// 强制走 slave
if(rrs.getRunOnSlave()){
LOGGER.debug("rrs.isHasBlanceFlag() " + rrs.isHasBlanceFlag());
if (rrs.isHasBlanceFlag()) { // 带有 /balance/ 注解(目前好像只支持一个注解…)
dbPool.getReadBanlanceCon(schema,autoCommit,handler, attachment, this.database);
}else{ // 没有 /balance/ 注解
LOGGER.debug(“rrs.isHasBlanceFlag()” + rrs.isHasBlanceFlag());
if(!dbPool.getReadCon(schema, autoCommit, handler, attachment, this.database)){
LOGGER.warn(“Do not have slave connection to use, use master connection instead.”);
PhysicalDatasource writeSource=dbPool.getSource();
//记录写节点写负载值
writeSource.setWriteCount();
writeSource.getConnection(schema,
autoCommit, handler, attachment);
rrs.setRunOnSlave(false);
rrs.setCanRunInReadDB(false);
}
}
}else{ // 强制走 master
// 默认获得的是 writeSource,也就是 走master
LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave());
PhysicalDatasource writeSource=dbPool.getSource();
//记录写节点写负载值
writeSource.setReadCount();
writeSource.getConnection(schema, autoCommit,
handler, attachment);
rrs.setCanRunInReadDB(false);
}
}else{ // 没有 /db_type=master/slave/ 注解,按照原来的处理方式
LOGGER.debug("rrs.getRunOnSlave() " + rrs.getRunOnSlave()); // null
if (rrs.canRunnINReadDB(autoCommit)) {
dbPool.getRWBanlanceCon(schema,autoCommit, handler, attachment, this.database);
} else {
PhysicalDatasource writeSource =dbPool.getSource();
//记录写节点写负载值
writeSource.setWriteCount();
writeSource.getConnection(schema, autoCommit,
handler, attachment);
}
}
} else {
throw new IllegalArgumentException(“Invalid DataSource:” + dbPool.getActivedIndex());
}
}
代码@1,参数说明:
-
String schema : 当前所在的mycat逻辑schema(数据库)
-
boolean autoCommit : 是否自动提交
-
RouteResultsetNode node:路由节点信息,代表一个datanode。
ResponseHandler handler:后端连接发送请求给后端mysql,返回结果后的处理hanlder,这里是SingleNodeHandler,如果是多节点处理的话,那就是MultiNodeHandler。
创建新的连接,在如下两个时机,一个是第一次初始化后端连接池,一次当初始化的连接数不够用的时候,需要再次创建
初次初始化连接池见:代码@2,第二个见PhysicalDatasource.getConnection中,如果连接池中的连接不够用的时候,会创建新的连接。
代码@3,这里涉及到读写分离注解的处理逻辑,本文不做详细解读,在后面的专题研究再做讲解。
我们就以PhysicalDatasource.getConnection为入口,继续跟踪连接的创建过程:
public void getConnection(String schema, boolean autocommit,
final ResponseHandler handler, final Object attachment)
throws IOException {
// 从当前连接map中拿取已建立好的后端连接
BackendConnection con = this.conMap.tryTakeCon(schema, autocommit); // @1
if (con != null) {
//如果不为空,则绑定对应前端请求的handler
takeCon(con, handler, attachment, schema); //@2
return;
} else {
int activeCons = this.getActiveCount();// 当前最大活动连接
if (activeCons + 1 > size) {// 下一个连接大于最大连接数
LOGGER.error(“the max activeConnnections size can not be max than maxconnections”);
throw new IOException(“the max activeConnnections size can not be max than maxconnections”);
} else { // create connection
LOGGER.info("no ilde connection in pool,create new connection for " + this.name + " of schema " + schema);
createNewConnection(handler, attachment, schema); //@3
}
}
}
代码@1:从PhysicalDatasource的map(连接池)中尝试获取一个连接。
代码@2:如果成功获取该连接,使用它并设置ResponseHandler等。
代码@3:调用createNewConnection创建一个新的连接
private void createNewConnection(final ResponseHandler handler,
final Object attachment, final String schema) throws IOException {
// aysn create connection
MycatServer.getInstance().getBusinessExecutor().execute(new Runnable() {
public void run() {
try {
createNewConnection(new DelegateResponseHandler(handler) { //@1
@Override
public void connectionError(Throwable e, BackendConnection conn) {
handler.connectionError(e, conn);
}
@Override
public void connectionAcquired(BackendConnection conn) {
takeCon(conn, handler, attachment, schema);
}
}, schema);
} catch (IOException e) {
handler.connectionError(e, null);
}
}
});
}
在业务线程池中异步执行创建连接并绑定前端命令执行中。不得不说,这是mycat追求更快响应速度的一个优化。
createNewConnection(new DelegateResponseHandler(handler) ,该方法在PhysicalDatasouce中是一个抽象方法:
我们关注MysqlDataSource实现类。
private final MySQLConnectionFactory factory;
public MySQLDataSource(DBHostConfig config, DataHostConfig hostConfig,
boolean isReadNode) {
super(config, hostConfig, isReadNode);
this.factory = new MySQLConnectionFactory();
}
@Override
public void createNewConnection(ResponseHandler handler,String schema) throws IOException {
factory.make(this, handler,schema);
}
主要调用MySQLDataSourceFactory的make方法。
public class MySQLConnectionFactory extends BackendConnectionFactory {
@SuppressWarnings({ “unchecked”, “rawtypes” })
public MySQLConnection make(MySQLDataSource pool, ResponseHandler handler,
String schema) throws IOException {
DBHostConfig dsc = pool.getConfig();
NetworkChannel channel = openSocketChannel(MycatServer.getInstance()
.isAIO()); // @1
MySQLConnection c = new MySQLConnection(channel, pool.isReadNode());
MycatServer.getInstance().getConfig().setSocketParams(c, false);
c.setHost(dsc.getIp());
c.setPort(dsc.getPort());
c.setUser(dsc.getUser());
c.setPassword(dsc.getPassword());
c.setSchema(schema);
c.setHandler(new MySQLConnectionAuthenticator(c, handler)); // @2
c.setPool(pool);
c.setIdleTimeout(pool.getConfig().getIdleTimeout());
if (channel instanceof AsynchronousSocketChannel) {
((AsynchronousSocketChannel) channel).connect(
new InetSocketAddress(dsc.getIp(), dsc.getPort()), c,
(CompletionHandler) MycatServer.getInstance()
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
架构学习资料
由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
));
if (channel instanceof AsynchronousSocketChannel) {
((AsynchronousSocketChannel) channel).connect(
new InetSocketAddress(dsc.getIp(), dsc.getPort()), c,
(CompletionHandler) MycatServer.getInstance()
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-mY5HY2UE-1713751182056)]
[外链图片转存中…(img-alHt26Yl-1713751182056)]
[外链图片转存中…(img-uaYgf4tv-1713751182057)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
架构学习资料
[外链图片转存中…(img-3O9ev3nZ-1713751182057)]
[外链图片转存中…(img-PIRYGEdG-1713751182057)]
[外链图片转存中…(img-bst5jSbd-1713751182057)]
[外链图片转存中…(img-xDgbuahk-1713751182057)]
[外链图片转存中…(img-jAuGSUgB-1713751182058)]
由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!