sách gpt4 ăn đã đi

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

In lại Tác giả: qq735679552 Thời gian cập nhật: 27-09-2022 22:32:09 33 4
mua khóa gpt4 giày nike

CFSDN nhấn mạnh vào giá trị tạo ra nguồn mở và chúng tôi cam kết xây dựng nền tảng chia sẻ tài nguyên để mọi nhân viên CNTT có thể tìm thấy thế giới tuyệt vời của bạn tại đây.

Bài viết blog CFSDN này phải đặt câu hỏi trong các cuộc phỏng vấn: Hãy nói về quá trình thực thi MyBatis? Được tác giả sưu tầm và biên soạn. Nếu các bạn quan tâm tới bài viết này thì nhớ like nhé.

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

Với sự phát triển của Internet, ngày càng nhiều công ty từ bỏ Hibernate và chọn theo đuổi MyBatis. Hơn nữa, nhiều nhà sản xuất lớn muốn hỏi về các nguyên tắc cơ bản và việc triển khai mã nguồn của MyBatis trong các cuộc phỏng vấn.

Nói tóm lại, MyBatis gần như đã trở thành một công nghệ khung mà các nhà phát triển Java phải nắm vững chuyên sâu. Hôm nay, chúng ta sẽ phân tích sâu về mã nguồn MyBatis. Bài viết hơi dài nên mình khuyên các bạn nên lưu lại trước rồi nghiên cứu từ từ nhé. Tổng chiều dài khoảng 30.000 từ và toàn bộ quá trình tốn nhiều năng lượng. Bạn bè có thể nghiên cứu từ từ.

Các bài viết đã được đưa vào:

https://github.com/sunshinelyz/technology-binghe 。

https://gitee.com/binghe001/technology-binghe 。

Cấu trúc chính của bài viết này như sau.

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

Phân tích mã nguồn MyBatis

Mọi người nên biết rằng mã nguồn Mybatis cũng là sự đóng gói lại của Jbdc. Dù được đóng gói như thế nào thì vẫn sẽ có các bước để lấy liên kết, prepareStatement, tham số đóng gói và thực hiện các bước này.

Cấu hình quá trình phân tích cú pháp

  1. Tài nguyên chuỗi = "mybatis-config.xml"
  2. //1. Đọc tệp mybatis-config.xml trong phần tài nguyên 
  3. InputStream inputStream = Resources.getResourceAsStream(tài nguyên); 
  4. //2. Sử dụng SqlSessionFactoryBuilder để tạo SqlSessionFactory 
  5. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 
  6. //3. Tạo SqlSession thông qua sqlSessionFactory 
  7. SqlSession sqlSession = sqlSessionFactory. openSession(); 

Resources.getResourceAsStream(resource) đọc tệp

  1. công cộng tĩnh InputStream getResourceAsStream(String resource) ném IOException { 
  2.  trở lại lấy Tài Nguyên Như Luồng(vô giá trị, tài nguyên); 
  3. }  
  4. //Loader được gán giá trị làvô giá trị 
  5. công cộng tĩnh InputStream getResourceAsStream(ClassLoader loader, String resource) ném IOException { 
  6.  Dòng đầu vào TRONG = classLoaderWrapper.getResourceAsStream(tài nguyên, bộ nạp); 
  7.  nếu như (TRONG == vô giá trị) { 
  8.   ném IOException mới("Không tìm thấy tài nguyên" + tài nguyên); 
  9.  }  
  10.  trở lại TRONG
  11. //classLoader làvô giá trị 
  12. công cộng InputStream getResourceAsStream(String resource, ClassLoader classLoader) { 
  13.  trở lại getResourceAsStream(tài nguyên, getClassLoaders(classLoader)); 
  14. }  
  15. //tải lớp classLoader 
  16. InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { 
  17.   (ClassLoader cl: classLoader) { 
  18.   nếu như (vô giá trị != cl) { 
  19.    //Tải luồng tệp đường dẫn đã chỉ định 
  20.    InputStream returnValue = cl.getResourceAsStream(tài nguyên); 
  21.    // Hiện nay, một số trình tải lớp muốn dẫn đầu này "/", vì vậy chúng tôi'sẽ thêm nó và thử lại nếu chúng tôi không't tìm tài nguyên 
  22.    nếu như (vô giá trị == giá trị trả về) { 
  23.     returnValue = cl.getResourceAsStream("/" + tài nguyên); 
  24.    }  
  25.    nếu như (vô giá trị != giá trị trả về) { 
  26.     trở lại giá trị trả về; 
  27.    } 
  28.   } 
  29.  }  
  30.  trở lại vô giá trị

Tóm tắt: Chủ yếu thông qua phương thức ClassLoader.getResourceAsStream() để lấy Tài nguyên theo đường dẫn lớp đã chỉ định.

Tạo SqlSessionFactory thông qua SqlSessionFactoryBuilder

  1. //SqlSessionFactoryBuilder là mẫu xây dựng 
  2. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 
  3. công cộng Xây dựng SqlSessionFactory(InputStream inputStream) { 
  4.  trở lại xây dựng(inputStream, vô giá trịvô giá trị); 
  5. //XMLConfigBuilder cũng ở chế độ trình tạo 
  6. công cộng SqlSessionFactory build(InputStream inputStream, String môi trường, Thuộc tính thuộc tính) { 
  7.  thử { 
  8.   Trình phân tích cú pháp XMLConfigBuilder = new XMLConfigBuilder(inputStream, môi trường, thuộc tính); 
  9.   trở lại xây dựng(parser.parse()); 
  10.  } catch (Ngoại lệ e) { 
  11.   ném ExceptionFactory.wrapException("Lỗi khi xây dựng SqlSession.", đ); 
  12.  } Cuối cùng { 
  13.   ErrorContext.instance().reset(); 
  14.   thử { 
  15.    Luồng đầu vào.đóng(); 
  16.   } bắt (IOException e) { 
  17.    // Cố ý phớt lờ. Thích lỗi trước đó. 
  18.   } 
  19.  } 
  20. //Tiếp theo nhập hàm tạo XMLConfigBuilder 
  21. công cộng XMLConfigBuilder(InputStream inputStream, Môi trường String, Thuộc tính props) { 
  22.  cái này(new XPathParser(inputStream, ĐÚNG VẬY, props, new XMLMapperEntityResolver()), môi trường, props); 
  23. // Sau khi nhập xong, khởi tạo Cấu hình 
  24. XMLConfigBuilder riêng tư (trình phân tích cú pháp XPathParser, môi trường String, thuộc tính props) { 
  25.  super(Cấu hình mới()); 
  26.  ErrorContext.instance().resource("Cấu hình SQL Mapper"); 
  27.  this.configuration.setVariables(props); 
  28.  this.parsed = SAI
  29.  this.environment = môi trường; 
  30.  this.parser = trình phân tích cú pháp; 
  31. // Trong số đó Parser.parse() chịu trách nhiệm phân tích cú pháp xml, build(configuration) tạo SqlSessionFactory 
  32. trở lại xây dựng(parser.parse()); 

Parser.parse() phân tích cú pháp xml.

  1. công cộng Cấu hình phân tích() { 
  2.  //Xác định xem có lặp lại phân tích hay không 
  3.  nếu (đã phân tích) { 
  4.   ném BuilderException mới("Mỗi XMLConfigBuilder chỉ có thể được sử dụng một lần."); 
  5.  }  
  6.  đã phân tích = ĐÚNG VẬY
  7.  //Đọc file cấu hình Cấu hình nút cấp 1 
  8.  parseConfiguration(parser.evalNode("/cấu hình")); 
  9.  trở lại cấu hình; 
  1. riêng tư void parseConfiguration(XNode root) { 
  2.  thử { 
  3.   // thẻ thuộc tính, được sử dụng để định cấu hình thông tin tham số, chẳng hạn như thông tin kết nối cơ sở dữ liệu phổ biến nhất 
  4.   thuộc tínhElement(root.evalNode("của cải")); 
  5.   Thiết lập thuộc tính = settingsAsProperties(root.evalNode("cài đặt")); 
  6.   loadCustomVfs(cài đặt); 
  7.   loadCustomLogImpl(cài đặt); 
  8.   //Hai cách để sử dụng bí danh thực thể: 1. Chỉ định một thực thể duy nhất 2. Chỉ định một gói; 
  9.   loại Bí danh Phần tử (root.evalNode("typeAliases")); 
  10.   // Trình cắm 
  11.   pluginElement(root.evalNode("các plugin")); 
  12.   // Được sử dụng để tạo đối tượng (khi dữ liệu cơ sở dữ liệu được ánh xạ vào đối tượng java) 
  13.   objectFactoryElement(root.evalNode("Nhà máy đối tượng")); 
  14.   objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); 
  15.   phản xạFactoryElement(root.evalNode("Nhà máy phản xạ")); 
  16.   settingsElement(cài đặt); 
  17.   // đọc Nó sau đó đối tượngFactory  vấn đề objectWrapperFactory #631 
  18.   //Môi trường cơ sở dữ liệu 
  19.   môi trườngElement(root.evalNode("môi trường")); 
  20.   databaseIdProviderElement(root.evalNode("databaseIdProvider")); 
  21.   //Chuyển đổi giữa kiểu cơ sở dữ liệu và kiểu dữ liệu Java 
  22.   loạiHandlerElement(root.evalNode("trình xử lý kiểu")); 
  23.   // Đây là phân tích về việc thêm, xóa, sửa đổi và truy vấn cơ sở dữ liệu 
  24.   mapperElement(root.evalNode("người lập bản đồ")); 
  25.  } catch (Ngoại lệ e) { 
  26.   ném BuilderException mới("Lỗi khi phân tích cấu hình SQL Mapper. Nguyên nhân: " + đ, đ); 
  27.  } 

Tóm tắt: ParseConfiguration hoàn tất việc phân tích cú pháp các thẻ trong cấu hình.

  1. private void mapperElement(XNode parent) ném ra ngoại lệ { 
  2.  nếu (cha mẹ != vô giá trị) { 
  3.     (XNode con: parent.getChildren()) { 
  4.    // Phân tích <>tên=""/> 
  5.    nếu như ("bưu kiện". bằng(child.getName())) { 
  6.     Gói ánh xạ chuỗi = child.getStringAttribute("tên"); 
  7.     // Đường dẫn gói được lưu trữ trong mapperRegistry 
  8.     cấu hình.addMappers(mapperPackage); 
  9.    } khác { 
  10.     // Phân tích cú pháp "" lớp="" tài nguyên=""
  11.     Tài nguyên chuỗi = child.getStringAttribute("tài nguyên"); 
  12.     Chuỗi url = child.getStringAttribute("url"); 
  13.     String mapperClass = child.getStringAttribute("lớp học"); 
  14.     nếu (tài nguyên != vô giá trị && url == vô giá trị && Lớp mapper == vô giá trị) { 
  15.      ErrorContext.instance().resource(tài nguyên); 
  16.      //Đọc file Mapper.xml 
  17.      InputStream inputStream = Resources.getResourceAsStream(tài nguyên); 
  18.      XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, 
  19.      cấu hình, tài nguyên, configuration.getSqlFragments()); 
  20.      mapperParser.parse(); 
  21.     } khác nếu (tài nguyên == vô giá trị && url != vô giá trị && Lớp mapper == vô giá trị) { 
  22.      ErrorContext.instance().resource(url); 
  23.      InputStream inputStream = Resources.getUrlAsStream(url); 
  24.      XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, 
  25.      cấu hình, url, configuration.getSqlFragments()); 
  26.      mapperParser.parse(); 
  27.     } khác nếu (tài nguyên == vô giá trị && url == vô giá trị && mapperClass != vô giá trị) { 
  28.      Lớp mapperInterface = Resources.classForName(mapperClass); 
  29.      cấu hình.addMapper(mapperInterface); 
  30.     } khác { 
  31.      ném BuilderException mới("Một phần tử ánh xạ chỉ có thể chỉ định một url, tài nguyên hoặc lớp, nhưng không được quá một."); 
  32.     } 
  33.    } 
  34.   } 
  35.  } 

Tóm tắt: Bằng cách phân tích tệp config.xml, lấy Môi trường và Cài đặt trong đó. Điều quan trọng là phân tích tất cả những điều sau đây và thêm chúng vào Cấu hình tương tự như trung tâm cấu hình và tất cả thông tin cấu hình đều có ở đây.

mapperParser.parse() phân tích trình ánh xạ Mapper.

  1. công cộng void phân tích() { 
  2.  nếu (!configuration.isResourceLoaded(tài nguyên)) { 
  3.   // Phân tích tất cả các thẻ phụ 
  4.   configurationElement(parser.evalNode("/người lập bản đồ")); 
  5.   cấu hình.addLoadedResource(tài nguyên); 
  6.   // Liên kết không gian tên (loại giao diện) và lớp nhà máy 
  7.   bindMapperForNamespace(); 
  8.  } 
  9.  phân tích cú phápPendingResultMaps(); 
  10.  phân tích cú phápPendingCacheRefs(); 
  11.  nhấnPendingStatements(); 
  12. }  
  13. // Cái được phân tích ở đây là thẻ của Mapper.xml 
  14. private void configurationElement(XNode context) { 
  15.  thử { 
  16.   Không gian tên chuỗi = context.getStringAttribute("không gian tên"); 
  17.   nếu (không gian tên == vô giá trị || không gian tên.bằng("")) { 
  18.    ném BuilderException mới("Không gian tên của người lập bản đồ không được để trống"); 
  19.   }  
  20.   builderAssistant.setCurrentNamespace(không gian tên); 
  21.   // Tham chiếu đến các cấu hình bộ đệm không gian tên khác 
  22.   cacheRefElement(context.evalNode("tham chiếu bộ nhớ đệm")); 
  23.   //Cấu hình bộ đệm cho một không gian tên nhất định 
  24.   cacheElement(context.evalNode("bộ nhớ đệm")); 
  25.   tham sốMapElement(bối cảnh.evalNodes("/mapper/Bản đồ tham số")); 
  26.   // là phần tử phức tạp và mạnh mẽ nhất, dùng để mô tả cách tải các đối tượng từ tập kết quả cơ sở dữ liệu 
  27.   resultMapElements(bối cảnh.evalNodes("/bản đồ/bản đồ kết quả")); 
  28.   //Khối câu lệnh có thể tái sử dụng và có thể được tham chiếu bởi các câu lệnh khác 
  29.   sqlElement(bối cảnh.evalNodes("/bản đồ/sql")); 
  30.   // Lấy đối tượng MappedStatement (thêm, xóa, sửa, kiểm tra thẻ) 
  31.   xây dựngStatementFromContext(bối cảnh.evalNodes("chọn|chèn|cập nhật|xóa")); 
  32.  } catch (Ngoại lệ e) { 
  33.   ném BuilderException mới("Lỗi khi phân tích cú pháp Mapper XML. Vị trí XML là '" + tài nguyên + "'. Gây ra: " + đ, đ); 
  34.  } 
  35. // Lấy đối tượng MappedStatement (thêm, xóa, sửa, kiểm tra thẻ) 
  36. riêng tư void buildStatementFromContext(Danh sách danh sách) { 
  37.  nếu (configuration.getDatabaseId() != vô giá trị) { 
  38.   buildStatementFromContext(danh sách, cấu hình.getDatabaseId()); 
  39.  }  
  40.  buildStatementFromContext(danh sách, vô giá trị); 
  41. // Lấy đối tượng MappedStatement (thêm, xóa, sửa, kiểm tra thẻ) 
  42. private void buildStatementFromContext(Danh sách danh sách, String requiredDatabaseId) { 
  43.  // Vòng lặp để thêm, xóa, sửa và kiểm tra thẻ 
  44.   (Bối cảnh XNode: danh sách) { 
  45.   câu lệnh XMLStatementBuilder cuối cùngParser = new XMLStatementBuilder(cấu hình, builderAssistant, ngữ cảnh, requiredDatabaseId); 
  46.   thử { 
  47.    // phân tích cú phápchèn/cập nhật/lựa chọnthẻ trong /del 
  48.    statementParser.parseStatementNode(); 
  49.   } bắt (IncompleteElementException e) { 
  50.    cấu hình.addIncompleteStatement(statementParser); 
  51.   } 
  52.  } 
  53. công cộng void parseStatementNode() { 
  54.  // Một mã định danh duy nhất trong không gian tên có thể được sử dụng để tham chiếu câu lệnh này 
  55.  Chuỗi id = context.getStringAttribute("nhận dạng"); 
  56.  //Nhận dạng nhà sản xuất cơ sở dữ liệu 
  57.  Chuỗi databaseId = context.getStringAttribute("ID cơ sở dữ liệu"); 
  58.  nếu (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { 
  59.   trở lại
  60.  }  
  61.  Chuỗi nodeName = context.getNode().getNodeName(); 
  62.  Kiểu lệnh sqlCommandType = 
  63.  SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); 
  64.  boolean isSelect = sqlCommandType == SqlCommandType.LỰA CHỌN
  65.  //flushCache và useCache đều liên quan đến bộ đệm cấp hai 
  66.  // Đặt nó thànhĐÚNG VẬYSau đó, miễn là câu lệnh được gọi, bộ đệm cục bộ và bộ đệm cấp hai sẽ bị xóa Giá trị mặc định:SAI 
  67.  boolean flushCache = ngữ cảnh.getBooleanAttribute("xả bộ nhớ đệm", !isSelect); 
  68.  // Đặt nó thành ĐÚNG VẬY Sau đó, kết quả của câu lệnh này sẽ được lưu vào bộ đệm cấp hai. Giá trị mặc định: Có. lựa chọn Các phần tử là ĐÚNG VẬY 
  69.  boolean useCache = ngữ cảnh.getBooleanAttribute("sử dụngCache", làChọn); 
  70.  boolean resultOrdered = ngữ cảnh.getBooleanAttribute("kết quả đã được sắp xếp"SAI); 
  71.  // Bao gồm các đoạn mã trước khi phân tích cú pháp 
  72.  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(cấu hình, builderAssistant); 
  73.  includeParser.applyIncludes(context.getNode()); 
  74.  // Tên hoặc bí danh đủ điều kiện của lớp tham số sẽ được chuyển vào câu lệnh này 
  75.  Kiểu tham số chuỗi = context.getStringAttribute("kiểu tham số"); 
  76.  Lớp parameterTypeClass = resolveClass(parameterType); 
  77.  Chuỗi lang = context.getStringAttribute("lạng"); 
  78.  Ngôn ngữDriver langDriver = getLanguageDriver(lang); 
  79.  // Phân tích selectKey sau đó bao gồm  loại bỏ chúng. 
  80.  processSelectKeyNodes(id, parameterTypeClass, langDriver); 
  81.  // Phân tích cú pháp SQL (trước:    đã được phân tích cú pháp  LOẠI BỎ) 
  82.  Trình tạo khóa keyGenerator; 
  83.  Chuỗi keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; 
  84.  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, ĐÚNG VẬY); 
  85.  nếu (configuration.hasKeyGenerator(keyStatementId)) { 
  86.   keyGenerator = configuration.getKeyGenerator(keyStatementId); 
  87.  } khác { 
  88.   keyGenerator = ngữ cảnh.getBooleanAttribute("sử dụng Khóa Tạo Ra", configuration.isUseGeneratedKeys() && SqlCommandType.CHÈN.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE: NoKeyGenerator.INSTANCE; 
  89.  }  
  90.  SqlSource sqlSource = langDriver.createSqlSource(cấu hình, ngữ cảnh, parameterTypeClass); 
  91.  Loại câu lệnh Loại câu lệnh = 
  92.  StatementType.valueOf(bối cảnh.getStringAttribute("Loại câu lệnh"
  93.  StatementType.PREPARED.toString())); 
  94.  Số nguyên getSize = bối cảnh.getIntAttribution("lấy kích thước"); 
  95.  Số nguyên thời gian chờ = context.getIntAttribute("hết giờ"); 
  96.  Tham số chuỗiMap = context.getStringAttribute("Bản đồ tham số"); 
  97.  // Tên hoặc bí danh đủ điều kiện của lớp thuộc loại mong muốn được trả về từ câu lệnh này 
  98.  Chuỗi resultType = context.getStringAttribute("Loại kết quả"); 
  99.  Lớp resultTypeClass = resolveClass(resultType); 
  100.  // Được đặt tên tham chiếu đến resultMap bên ngoài 
  101.  Chuỗi resultMap = context.getStringAttribute("bản đồ kết quả"); 
  102.  Chuỗi resultSetType = context.getStringAttribute("Kết quả thiết lập loại"); 
  103.  Kiểu kết quảSetType resultSetTypeEnum = giải quyếtKiểu kết quảSet(Kiểu kết quảSet); 
  104.  Chuỗi keyProperty = context.getStringAttribute("Thuộc tính khóa"); 
  105.  Chuỗi keyColumn = context.getStringAttribute("Cột khóa"); 
  106.  Chuỗi resultSets = context.getStringAttribute("resultSets"); 
  107.  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, 
  108.  fetchSize, thời gian chờ, parameterMap, parameterTypeClass, resultMap, resultTypeClass, 
  109.  resultSetTypeEnum, flushCache, useCache, resultOrdered, 
  110.  keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 
  111. công cộng MappedStatement thêmMappedStatement( 
  112.  Chuỗi id, 
  113.  Nguồn Sql sqlSource, 
  114.  Loại câu lệnh Loại câu lệnh, 
  115.  Kiểu lệnh sqlCommandType, 
  116.  Số nguyên lấy kích thước, 
  117.  Số nguyên hết giờ, 
  118.  Tham số chuỗiMap, 
  119.  Lớp tham sốType, 
  120.  Chuỗi resultMap, 
  121.  Lớp resultType, 
  122.  Kiểu tập kết quả, 
  123.  boolean flushCache, 
  124.  boolean useCache, 
  125.  boolean resultOrdered, 
  126.  Máy tạo khóa máy tạo khóa, 
  127.  Chuỗi keyProperty, 
  128.  Chuỗi keyColumn, 
  129.  Chuỗi databaseId, 
  130.  Ngôn ngữDriver lang, 
  131.  Chuỗi resultSets) { 
  132.  nếu (unresolvedCacheRef) { 
  133.   ném IncompleteElementException mới("Cache-ref vẫn chưa được giải quyết"); 
  134.  }  
  135.   id = applyCurrentNamespace(id, SAI); 
  136.   boolean isSelect = sqlCommandType == SqlCommandType.LỰA CHỌN
  137.   MappedStatement.Builder statementBuilder = new MappedStatement.Builder(cấu hình, 
  138.   id, sqlSource, sqlCommandType) 
  139.   .resource(tài nguyên) 
  140.   .fetchSize(Kích thước lấy) 
  141.   .timeout(hết giờ) 
  142.   .statementType(statementType) 
  143.   .keyGenerator(trình tạo khóa) 
  144.   .keyProperty(Thuộc tính khóa) 
  145.   .keyColumn(Cột khóa) 
  146.   .databaseId(Id cơ sở dữ liệu) 
  147.   .chỉ một 
  148.   .resultOrdered(kết quả đã được đặt hàng) 
  149.   .resultSets(các tập kết quả) 
  150.   .resultMaps(getStatementResultMaps(resultMap, resultType, id)) 
  151.   .resultSetType(Kết quả thiết lập loại) 
  152.   .flushCacheRequired(giá trị hoặc mặc định(flushCache, !isSelect)) 
  153.   .useCache(giá trị hoặc mặc định(useCache, isSelect)) 
  154.   .cache(bộ nhớ đệm hiện tại); 
  155.   Câu lệnh ParameterMap = getStatementParameterMap(parameterMap, 
  156.   Kiểu tham số, id); 
  157.   nếu (statementParameterMap != vô giá trị) { 
  158.    statementBuilder.parameterMap(statementParameterMap); 
  159.   }  
  160.   Câu lệnh MappedStatement = statementBuilder.build(); 
  161.   //Giữ cấu hình 
  162.   configuration.addMappedStatement(câu lệnh); 
  163.   trở lại tuyên bố; 
  164. công cộng void addMappedStatement(MappedStatement ms){ 
  165. //ms.getId = mapper.UserMapper.getUserById 
  166. //ms = MappedStatement bằng dữ liệu trong mỗi thẻ được thêm, xóa, sửa đổi và kiểm tra 
  167.  mappedStatements.put(ms.getId(), ms); 
  168. // Cuối cùng được lưu trữ trong mappedStatements, mappedStatements lưu trữ các phần bổ sung, xóa, sửa đổi và truy vấn từng cái một 
  169. được bảo vệ Bản đồ cuối cùng mappedStatement = new StrictMap ("Bộ sưu tập các câu lệnh đã ánh xạ").conflictMessageProducer((giá trị đã lưu, giá trị mục tiêu) -> 
  170. ". vui lòng kiểm tra " + savedValue.getResource() + " Và " + targetValue.getResource()); 

Phân tích phương thức bindMapperForNamespace().

Liên kết không gian tên (loại giao diện) với lớp nhà máy.

  1. riêng tư void bindMapperForNamespace() { 
  2.  // Không gian tên của Mapper hiện tại 
  3.  Không gian tên chuỗi = builderAssistant.getCurrentNamespace(); 
  4.  nếu (không gian tên != vô giá trị) { 
  5.   Lớp boundType = vô giá trị
  6.   thử { 
  7.    //giao diện Mapper.UserMapper loại này 
  8.    boundType = Resources.classForName(không gian tên); 
  9.   } bắt (ClassNotFoundException e) { 
  10.   }  
  11.   nếu (boundType != vô giá trị) { 
  12.    nếu (!configuration.hasMapper(boundType)) { 
  13.     cấu hình.addLoadedResource("không gian tên:" + không gian tên); 
  14.     cấu hình.addMapper(boundType); 
  15.    } 
  16.   } 
  17.  } 
  18. công cộng  void addMapper(Class loại) { 
  19.  mapperRegistry.addMapper(kiểu); 
  20. }  
  21. công cộng  void addMapper(Class loại) { 
  22.  nếu (type.isInterface()) { 
  23.   nếu (hasMapper(type)) { 
  24.    ném BindingException mới("Kiểu " + loại + "đã được MapperRegistry biết đến."); 
  25.   }  
  26.   boolean loadCompleted = SAI
  27.   thử { 
  28.    //kiểu giao diện(chìa khóa)->Lớp nhà máy 
  29.    knownMappers.put(kiểu, MapperProxyFactory mới<>(kiểu)); 
  30.    Bộ phân tích cú pháp MapperAnnotationBuilder = new MapperAnnotationBuilder(config, type); 
  31.    parser.parse(); 
  32.    tảiHoàn thành = ĐÚNG VẬY
  33.   } Cuối cùng { 
  34.    nếu (!loadCompleted) { 
  35.     knownMappers.remove(kiểu); 
  36.    } 
  37.   } 
  38.  } 

Tạo đối tượng SqlSessionFactory.

Phương thức XMLMapperBuilder.parse() được sử dụng để phân tích trình ánh xạ Mapper. Có hai phương thức:

(1) configureElement() phân tích tất cả các thẻ phụ và cuối cùng phân tích id (đường dẫn đầy đủ) của thẻ chèn/cập nhật/xóa/chọn trong Mapper.xml để tạo khóa và toàn bộ thẻ cũng như kết nối dữ liệu để tạo thành MappedStatement và lưu trữ nó trong mappedStatements trong Cấu hình.

(2) bindMapperForNamespace() lưu trữ loại giao diện (giao diện ánh xạ.UserMapper) và lớp nhà máy vào knownMappers trong MapperRegistry.

Tạo SqlSessionFactory

  1. công cộng SqlSessionFactory build(Cấu hình cấu hình) { 
  2.  trở lại mới DefaultSqlSessionFactory(config); 

Trực tiếp sử dụng Cấu hình làm tham số và trực tiếp tạo DefaultSqlSessionFactory mới.

Quá trình tạo phiên SqlSession

Mỗi khi mybatis kết nối với cơ sở dữ liệu, một phiên cần được tạo. Chúng tôi sử dụng phương thức openSession() để tạo phiên. Phiên này cần chứa Executor để thực thi SQL. Người thực thi cũng cần chỉ định loại giao dịch và loại người thực thi.

Tạo giao dịch (hai cách).

tài sản Tạo lớp nhà máy tạo giao dịch
JDBC Nhà máy giao dịch Jbdc Giao dịch Jdbc
ĐÃ QUẢN LÝ Nhà máy giao dịch được quản lý Giao dịch được quản lý
  • Nếu JDBC được định cấu hình, commit(), rollback() và close() của đối tượng Connection sẽ được sử dụng để quản lý các giao dịch.
  • Nếu được định cấu hình là QUẢN LÝ, giao dịch sẽ được chuyển giao cho vùng chứa để quản lý, chẳng hạn như JBOSS và Weblogic.
  1. SqlSession sqlSession = sqlSessionFactory. openSession(); 
  1. công cộng Phiên mở SqlSession() { 
  2.  // Có một phép gán mặc định trong ExecutorType được bảo vệ bằng cấu hình defaultExecutorType = ExecutorType.SIMPLE 
  3.  trở lại openSessionFromDataSource(cấu hình.getDefaultExecutorType(), vô giá trịSAI); 
  1. mặc định="phát triển"
  2.  "phát triển"
  3.   "JDBC"/> 
  4.   "ĐÃ TỔNG HỢP"
  5.    tên="tài xế" giá trị="${trình điều khiển}"/> 
  6.    tên="url" giá trị="${url}"/> 
  7.    tên="tên người dùng" giá trị="${tên người dùng}"/> 
  8.    tên="mật khẩu" giá trị="${mật khẩu}"/> 
  9.    
  10.   
  11.  

Tạo người thi hành

  1. //ExecutorType là SIMPLE, có 3 loại SIMPLE (SimpleExecutor), REUSE (ReuseExecutor), BATCH (BatchExecutor) 
  2. riêng tư SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel mức độ, boolean tự động cam kết) { 
  3.  Giao dịch tx = vô giá trị
  4.  thử { 
  5.   // nút phát triển trong xml 
  6.   Môi trường cuối cùng environment = configuration.getEnvironment(); 
  7.   // Cấu hình kiểu là Jbdc, do đó lớp nhà máy JbdcTransactionFactory được tạo. 
  8.   TransactionFactory cuối cùng transactionFactory = getTransactionFactoryFromEnvironment(môi trường); 
  9.   //Jdbc tạo JbdcTransactionFactory tạo JbdcTransaction 
  10.   tx = transactionFactory.newTransaction(môi trường.getDataSource(), mức độ, tự động cam kết); 
  11.   //Tạo bộ thực thi CachingExecutor 
  12.   Executor cuối cùng executor = configuration.newExecutor(tx, execType); 
  13.   //Tạo các thuộc tính DefaultSqlSession bao gồm các đối tượng Configuration và Executor 
  14.   trở lại new DefaultSqlSession(cấu hình, thực thi, tự động cam kết); 
  15.  } catch (Ngoại lệ e) { 
  16.   closeTransaction(tx); // có thể đã lấy được một sự liên quan vậy hãy gọi 
  17.   đóng() 
  18.   ném ExceptionFactory.wrapException("Lỗi khi mở phiên. Nguyên nhân: " + đ, đ); 
  19.  } Cuối cùng { 
  20.   ErrorContext.instance().reset(); 
  21.  } 

Nhận đối tượng Mapper

  1. Người dùngMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  1. công cộng T getMapper(Kiểu lớp) { 
  2.  
  3. trở lại configuration.getMapper(kiểu, cái này); 
  4.  

mapperRegistry.getMapper được lấy từ knownMappers của MapperRegistry. KnownMappers lưu trữ loại giao diện (giao diện mapper.UserMapper) và lớp nhà máy (MapperProxyFactory).

  1. công cộng  T getMapper(Class loại, SqlSession sqlSession) { 
  2.  trở lại mapperRegistry.getMapper(kiểu, sqlSession); 

Lấy lớp nhà máy tương ứng từ Bản đồ của knownMappers theo loại giao diện (interface mapper.UserMapper).

  1. công cộng  T getMapper(Class loại, SqlSession sqlSession) { 
  2.  MapperProxyFactory cuối cùng mapperProxyFactory = (MapperProxyFactory
  3.  knownMappers.get(kiểu); 
  4.  nếu (mapperProxyFactory == vô giá trị) { 
  5.   ném BindingException mới("Kiểu " + loại + "không được MapperRegistry biết đến."); 
  6.  }  
  7.  thử { 
  8.   trở lại mapperProxyFactory.newInstance(sqlSession); 
  9.  } catch (Ngoại lệ e) { 
  10.   ném BindingException mới("Lỗi khi nhận phiên bản mapper. Nguyên nhân: " + đ, đ); 
  11.  } 
  12. công cộng T newInstance(SqlSession sqlSession) { 
  13.  MapperProxy cuối cùng mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); 
  14.  trở lại newInstance(mapperProxy); 

Ở đây, đối tượng proxy MapperProxy (org.apache.ibatis.bind.MapperProxy@6b2ea799) được trả về thông qua proxy động JDK.

  1. được bảo vệ T newInstance(MapperProxy mapperProxy) { 
  2.  //mapperInterface là giao diện mapper.UserMapper  
  3.  trở lại (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), mới 
  4.  Lớp[] { mapperInterface }, mapperProxy); 
  1. Người dùngMapper userMapper = sqlSession.getMapper(UserMapper.class); 

Thực thi SQL

  1. Người sử dụng người sử dụng = userMapper.getUserById(1); 

Gọi phương thức proxy gọi

Vì tất cả Người lập bản đồ đều là đối tượng proxy của MapperProxy nên bất kỳ phương thức nào cũng thực thi phương thức gọi() của MapperProxy.

  1. công cộng Đối tượng invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  2.  thử { 
  3.   //Xác định xem bạn cần thực thi SQL hay thực thi trực tiếp phương thức 
  4.   nếu (Object.class.equals(phương thức.getDeclaringClass())) { 
  5.    trở lại phương thức.invoke(this, args); 
  6.    // Cái được đánh giá ở đây là phương thức mặc định trong giao diệnMặc địnhChờ đợi 
  7.   } khác nếu (isDefaultMethod(phương thức)) { 
  8.    trở lại invokeDefaultMethod(proxy, phương thức, đối số); 
  9.   } 
  10.  } catch (Có thể ném t) { 
  11.   ném ExceptionUtil.unwrapThrowable(t); 
  12.  }  
  13.     // Lấy bộ đệm để lưu mối quan hệ giữa chữ ký phương thức và phương thức giao diện 
  14.  MapperMethod cuối cùng mapperMethod = cachedMapperMethod(phương pháp); 
  15.  trở lại Phương pháp mapper.thực hiện(sqlSession, đối số); 

Phương thức thực hiện cuộc gọi

Ví dụ được sử dụng ở đây sử dụng một truy vấn, vì vậy câu lệnh nhánh else sẽ được sử dụng.

  1. công cộng Sự vật thực hiện(SqlSession sqlSession, Đối tượng[] args) { 
  2.  Kết quả đối tượng; 
  3.  //Thao tác command.getType() không thể được thực hiện theo loại lệnh làlựa chọn 
  4.  chuyển đổi (command.getType()) { 
  5.   trường hợp CHÈN: { 
  6.    Đối tượng param = method.convertArgsToSqlCommandParam(args); 
  7.    kết quả = rowCountResult(sqlSession.chèn(lệnh.getName(), tham số)); 
  8.    phá vỡ; 
  9.   }  
  10.   trường hợp CẬP NHẬT: { 
  11.    Đối tượng param = method.convertArgsToSqlCommandParam(args); 
  12.    kết quả = rowCountResult(sqlSession.cập nhật(lệnh.getName(), tham số)); 
  13.    phá vỡ; 
  14.   }  
  15.   trường hợp XÓA BỎ: { 
  16.    Đối tượng param = method.convertArgsToSqlCommandParam(args); 
  17.    kết quả = rowCountResult(sqlSession.delete(lệnh.getName(), tham số)); 
  18.    phá vỡ; 
  19.   }  
  20.   trường hợp LỰA CHỌN
  21.    nếu (phương thức.returnsVoid() && phương thức.hasResultHandler()) { 
  22.     thực thiWithResultHandler(sqlSession, đối số); 
  23.     kết quả = vô giá trị
  24.    } khác nếu (phương thức.returnsMany()) { 
  25.     kết quả = executeForMany(sqlSession, args); 
  26.    } khác nếu (phương thức.returnsMap()) { 
  27.     kết quả = executeForMap(sqlSession, args); 
  28.    } khác nếu (phương thức.returnsCursor()) { 
  29.     kết quả = executeForCursor(sqlSession, args); 
  30.    } khác { 
  31.     //Chuyển đổi tham số thành tham số SQL 
  32.     Đối tượng param = method.convertArgsToSqlCommandParam(args); 
  33.     kết quả = sqlSession.selectOne(command.getName(), param); 
  34.     nếu (phương thức.returnsOptional() 
  35.     && (kết quả == vô giá trị || 
  36.     !phương thức.getReturnType().bằng(result.getClass())) { 
  37.      kết quả = Optional.ofNullable(kết quả); 
  38.     } 
  39.    } 
  40.    phá vỡ; 
  41.   trường hợp TUÔN RA: 
  42.    kết quả = sqlSession.flushStatements(); 
  43.    phá vỡ; 
  44.   mặc định
  45.    ném BindingException mới("Phương pháp thực hiện không xác định cho: " + lệnh.getName()); 
  46.  }  
  47.  nếu (kết quả == vô giá trị && Method.getReturnType().isPrimitive() && !method.returnsVoid()) { 
  48.   ném BindingException mới("Phương pháp lập bản đồ '" + lệnh.getName() + " đã cố gắng trả về giá trị null từ một phương thức có kiểu trả về nguyên thủy (" + phương thức.getReturnType() + ")."); 
  49.  }  
  50.  trở lại kết quả; 

Gọi selectOne thực sự là selectList

SelectOne truy vấn một và truy vấn nhiều truy vấn thực sự giống nhau.

  1. công cộng  T selectOne(Câu lệnh String, Tham số đối tượng) { 
  2.  // Phiếu bầu phổ thông là ĐẾN trở lại vô giá trị TRÊN 0 kết quả  ném ngoại lệ TRÊN quá nhiều. 
  3.  List list = this.selectList(câu lệnh, tham số); 
  4.  nếu (danh sách.kích cỡ() == 1) { 
  5.   trở lại danh sách.get(0); 
  6.  } khác nếu (danh sách.kích cỡ() > 1) { 
  7.   ném TooManyResultsException mới("Dự kiến ​​một kết quả (hoặc null) được trả về bởi selectOne(), nhưng tìm thấy: " + danh sách.kích cỡ()); 
  8.  } khác { 
  9.   trở lại vô giá trị
  10.  } 
  1. công cộng  Danh sách selectList(Câu lệnh String, Tham số đối tượng, RowBounds rowBounds) { 
  2.  thử { 
  3.   //Theo mappedStatements trong Cấu hìnhchìa khóa(Đường dẫn đầy đủ của id) Lấy đối tượng MappedStatement 
  4.   MappedStatement ms = configuration.getMappedStatement(câu lệnh); 
  5.   trở lại executor.query(ms, wrapCollection(tham số), rowBounds, Executor.NO_RESULT_HANDLER); 
  6.  } catch (Ngoại lệ e) { 
  7.   ném ExceptionFactory.wrapException("Lỗi khi truy vấn cơ sở dữ liệu. Nguyên nhân: " + đ, đ); 
  8.  } Cuối cùng { 
  9.   ErrorContext.instance().reset(); 
  10.  } 

Đối tượng mappedStatements được hiển thị trong hình.

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

Đối tượng MappedStatement được hiển thị trong hình.

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

Thực hiện phương thức truy vấn

Tạo CacheKey.

Lấy thông tin SQL từ BoundSql và tạo CacheKey. CacheKey này là Khóa được lưu trong bộ nhớ cache.

  1. công cộng  Danh sách truy vấn(MappedStatement ms, Object tham sốObject, RowBounds rowBounds, ResultHandler resultHandler) ném SQLException { 
  2.  //Tạo bộ đệmChìa khóa 
  3.  BoundSql boundSql = ms.getBoundSql(tham sốObject); 
  4.  //chìa khóa = -575461213:-771016147:mapper.UserMapper.getUserById:0:2147483647:lựa chọn * từ người dùng thử nghiệm Ở đâu id = ?:1:phát triển 
  5.  Khóa bộ nhớ đệm chìa khóa = createCacheKey(ms, parameterObject, rowBounds, boundSql); 
  6.  trở lại truy vấn(ms, parameterObject, rowBounds, resultHandler, chìa khóa, boundSql); 
  1. công cộng  Danh sách truy vấn(MappedStatement ms, Đối tượng tham sốObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey chìa khóa, BoundSql boundSql) ném SQLException { 
  2.  Bộ nhớ đệm bộ nhớ đệm = ms.getCache(); 
  3.  nếu (bộ nhớ đệm != vô giá trị) { 
  4.   flushCacheIfRequired(ms); 
  5.   nếu (ms.isUseCache() && resultHandler == vô giá trị) { 
  6.    đảm bảoNoOutParams(ms, boundSql); 
  7.    @SuppressCảnh báo("chưa được kiểm tra"
  8.    Danh sách danh sách = (Danh sách) tcm.getObject(bộ nhớ đệm, chìa khóa); 
  9.    nếu (danh sách == vô giá trị) { 
  10.     danh sách = delegate.query(ms, parameterObject, rowBounds, resultHandler, chìa khóa, boundSql); 
  11.     tcm.putObject(bộ nhớ đệm, chìa khóa, danh sách); // vấn đề #578  #116 
  12.    }  
  13.    trở lại danh sách; 
  14.   } 
  15.  } 
  16.  trở lại delegate.query(ms, parameterObject, rowBounds, resultHandler, chìa khóa, boundSql); 

Xóa bộ nhớ đệm cục bộ.

  1. công cộng  Danh sách truy vấn(MappedStatement ms, Tham số đối tượng, RowBounds rowBounds, ResultHandler resultHandler, CacheKey chìa khóa, BoundSql boundSql) ném SQLException { 
  2.  ErrorContext.instance().resource(ms.getResource()).activity("thực hiện một truy vấn").đối tượng(ms.getId()); 
  3.  nếu (đóng) { 
  4.   ném ExecutorException mới("Người thực hiện đã đóng."); 
  5.  }  
  6.  //queryStack được sử dụng để ghi lại ngăn xếp truy vấn nhằm ngăn các truy vấn đệ quy xử lý nhiều lần bộ đệm. 
  7.  //xả bộ nhớ đệm=ĐÚNG VẬY Khi đó, bộ đệm cục bộ (bộ đệm cấp một) sẽ bị xóa trước tiên. 
  8.  nếu (queryStack == 0 && ms.isFlushCacheRequired()) { 
  9.   //Xóa bộ đệm cục bộ 
  10.   xóa bộ nhớ đệm cục bộ(); 
  11.  }  
  12.  Danh sách
  13.  thử { 
  14.   truy vấnStack++; 
  15.   danh sách = resultHandler == vô giá trị ? (Danh sách) localCache.getObject(chìa khóa) : vô giá trị
  16.   nếu (danh sách != vô giá trị) { 
  17.    handleLocallyCachedOutputParameters(ms, chìa khóa, tham số, boundSql); 
  18.   } khác { 
  19.    // Nếu không có bộ đệm, nó sẽ được truy vấn từ cơ sở dữ liệu: queryFromDatabase() 
  20.    danh sách = queryFromDatabase(ms, tham số, rowBounds, resultHandler, chìa khóa, boundSql); 
  21.   } 
  22.  } Cuối cùng { 
  23.   truy vấnStack--; 
  24.  }  
  25.  nếu (queryStack == 0) { 
  26.    (Trì hoãn tải trọng trì ... 
  27.   Tải chậm.trọng tải(); 
  28.   }  
  29.   // số #601 
  30.   deferredLoads. xóa(); 
  31.   // Nếu LocalCacheScope == STATEMENT, bộ đệm cục bộ sẽ bị xóa 
  32.   nếu (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { 
  33.    // số 482 
  34.    xóa bộ nhớ đệm cục bộ(); 
  35.   } 
  36.  }  
  37.  trở lại danh sách; 

Truy vấn từ cơ sở dữ liệu.

  1. private List queryFromDatabase(MappedStatement ms, Tham số đối tượng, RowBounds rowBounds, ResultHandler resultHandler, CacheKey chìa khóa, BoundSql boundSql) ném SQLException { 
  2.  Danh sách
  3.  // Lần đầu tiên sử dụng phần giữ chỗ trong bộ đệm 
  4.  localCache.putObject(chìa khóa, NGƯỜI GIỮ CHỖ_THỰC HIỆN); 
  5.  thử { 
  6.   //Thực thi doQuery() của Executor, mặc định là SimpleExecutor 
  7.   danh sách = doQuery(ms, tham số, rowBounds, resultHandler, boundSql); 
  8.  } Cuối cùng { 
  9.   // Sau khi thực hiện truy vấn, hãy xóa phần giữ chỗ 
  10.   localCache.removeObject(chìa khóa); 
  11.  }  
  12.  //chèn lại dữ liệu 
  13.  localCache.putObject(chìa khóa, danh sách); 
  14.  nếu (ms.getStatementType() == StatementType.CALLABLE) { 
  15.   localOutputParameterCache.putObject(chìa khóa, tham số); 
  16.  }  
  17.  trở lại danh sách; 

Thực thi doQuery.

  1. công cộng  Danh sách doQuery(MappedStatement ms, Tham số đối tượng, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) ném SQLException { 
  2.  Câu lệnh stmt = vô giá trị
  3.  thử { 
  4.   Cấu hình cấu hình = ms.getConfiguration(); 
  5.   Trình xử lý StatementHandler = configuration.newStatementHandler(wrapper, ms, tham số, rowBounds, resultHandler, boundSql); 
  6.   stmt = prepareStatement(trình xử lý, ms.getStatementLog()); 
  7.   trở lại handler.query(stmt, resultHandler); 
  8.  } Cuối cùng { 
  9.   đóng câu lệnh(stmt); 
  10.  } 

Tóm tắt mã nguồn

Nói chung, mã nguồn của MyBatis tương đối đơn giản, chỉ cần mọi người bỏ tâm trí và dành hai hoặc ba ngày để nghiên cứu kỹ thì về cơ bản họ có thể hiểu được ngữ cảnh chính của mã nguồn. Để giúp bạn bè dễ hiểu, Binghe đã biên soạn một sơ đồ quy trình thực hiện tổng thể của MyBatis.

Những câu hỏi phải đặt trong các cuộc phỏng vấn: Hãy nói về quá trình thực hiện MyBatis?

Được rồi, hôm nay thế thôi, tôi là Binghe, hẹn gặp lại lần sau~~.

Liên kết gốc: https://mp.weixin.qq.com/s/b1NTCG45WvFZLch9aN1dXg.

Cuối cùng, bài viết này nói về các câu hỏi phỏng vấn: Hãy nói về quá trình thực thi MyBatis? Bài viết chỉ vậy thôi. Nếu bạn muốn biết thêm về cuộc phỏng vấn, bạn phải hỏi: Hãy nói về quá trình thực hiện MyBatis? Về nội dung, vui lòng tìm kiếm các bài viết của CFSDN hoặc tiếp tục duyệt các bài viết liên quan. Tôi hy vọng bạn sẽ ủng hộ blog của tôi trong tương lai! .

33 4 0
qq735679552
Hồ sơ

Tôi là một lập trình viên xuất sắc, rất giỏi!

Nhận phiếu giảm giá taxi Didi miễn phí
Phiếu giảm giá taxi Didi
Chứng chỉ ICP Bắc Kinh số 000000
Hợp tác quảng cáo: 1813099741@qq.com 6ren.com
Xem sitemap của VNExpress