源码分析mycat1(2)

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开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

架构学习资料

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,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

架构学习资料

[外链图片转存中…(img-3O9ev3nZ-1713751182057)]

[外链图片转存中…(img-PIRYGEdG-1713751182057)]

[外链图片转存中…(img-bst5jSbd-1713751182057)]

[外链图片转存中…(img-xDgbuahk-1713751182057)]

[外链图片转存中…(img-jAuGSUgB-1713751182058)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值