from:http://blog.zenika.com/index.php?post/2012/03/28/Presentation-de-MyBatis

Mybatis,Springjdbc,Hibernate ,JDBC的多表映射学习比较

A l’occasion de la sortie de MyBatis 3.1.0 je vous propose de découvrir cet ORM pas comme les autres. Avec un niveau d’abstraction à mi-chemin entre JDBC et JPA, le successeur d’iBatis est activement développé : plusieurs mises à jour mineures se sont succédées avant l’arrivée de la version 3.1.0.

L'ORM pas comme les autres

MyBatis est un framework de persistance de base de données relationnelles, c'est un ORM au même titre que les différentes implémentations JPA : il transforme des données issues d'une base de données relationnelles en objets Java. La différence majeure entre JPA et MyBatis, c'est que le premier opère un mapping des objets sur des tables (les entités), alors que le second ne travaille qu'avec des requêtes. JPA crée un couplage entre la base de données et le modèle objet par le biais d'annotations (ou en XML) là où MyBatis offre beaucoup de souplesse pour le mapping. Il est même possible de mapper les résultats sur des classes qu'on ne peut pas modifier (classes issues de bibliothèques tierces ou développées par une autre équipe par exemple).

Principales fonctionnalités:

  • Création de requêtes SQL dynamiques
  • Mapping avancé des résultats
  • Personnalisation du mapping
  • Gestion des transactions
  • Mise en cache des résultats
  • Pool de connexion
  • Configuration par environnement
  • Intégration à Spring et Guice

En terme d'outillage on trouve MyBatis Generator qui permet de démarrer son projet en générant des classes et des mappers de type CRUD. Un plugin Eclipse permet de faciliter l'affichage du SQL dynamique. Il existe également une version .NET de MyBatis.

Un cas d'utilisation simple

Afin d'illustrer l'utilisation de MyBatis, je vous propose de prendre un modèle de données simple en entrée, et un modèle de données différent en sortie. Le modèle en entrée est représenté par des tables dans une base de données relationnelle, et le modèle en sortie est représenté par des POJO. Entre les deux : un DAO qu'il faut implémenter pour passer de notre base de données à nos objets Java. Ajoutons comme hypothèse que les classes et l'interface ne peuvent pas être modifiées.

public class Session {private Long idFormation = null;private String nomCours = null;private String nomFormateur = null;private Set<Participant> participants = new HashSet<>();...
}
public class Participant {private Long id = null;private String nom = null;...
}
public interface SessionDao {/*** Récupère la liste des sessions de formation auxquelles des* stagiaires se sont inscrits.*/public List<Session> findAll();
}

Et on va se prêter à l'exercice avec du JDBC brut, JdbcTemplate, MyBatis et Hibernate. Il s'agit d'implémentationsnaïves, qui se contentent d'être correctes fonctionnellement. Les aspects tels que les best practices ou le tuning de performance ne seront donc pas abordés. Les différentes solutions sont implémentées sous la forme de tests junit et permet de s'assurer que le résultat obtenu est correct. Le code source complet est disponible à la fin de l'article.

Le résultat attendu est une liste de deux sessions, comptant trois participants chacune. D’un point de vue technique, le DAO renvoie une liste de sessions avec une relation 1-n vers des participants.

JDBC

Il s'agit là d'une API vieille de plus de 10 ans. JDBC est incontournable pour accéder à une base de données relationnelle, c'est d'ailleurs sur cette API que reposent les trois autres solutions de ce comparatif. L'ennui c'est que certaines fonctionnalités peuvent être supportées par certains drivers mais pas d'autres. Si on ajoute le fait qu'il s'agit d'une API très bas niveau, le JDBC brut n'est pas la solution à privilégier pour développer nos applications.

public class JdbcSessionDao implements SessionDao {private static final String FIND_ALL = "select fo.id as id_formation, co.nom as nom_cours, fe.nom as nom_formateur, st.id as id_stagiaire, st.nom as nom_stagiaire " +"from formation fo, cours co, formateur fe, stagiaire st " +"where co.id = fo.id_cours and fe.id = fo.id_formateur and fo.id = st.id_formation " +"order by id_formation";@Overridepublic List<Session> findAll() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DriverManager.getConnection("jdbc:h2:mem:dataSource;DB_CLOSE_DELAY=-1", "sa", "");statement = connection.createStatement();resultSet = statement.executeQuery(FIND_ALL);Session session = null;List<Session> sessions = new ArrayList<>();while ( resultSet.next() ) {Long idFormation = resultSet.getLong("id_formation");// pour mapper la session il faut vérifier le changement d'idif ( isNewSession(session, idFormation) ) {session = new Session();session.setIdFormation(idFormation);session.setNomCours( resultSet.getString("nom_cours") );session.setNomFormateur( resultSet.getString("nom_formateur") );sessions.add(session);}// dans le cas des participants pas besoin de vérifier (d'après la requête)Participant participant = new Participant();participant.setId( resultSet.getLong("id_stagiaire") );participant.setNom( resultSet.getString("nom_stagiaire") );session.getParticipants().add(participant);}return sessions;}catch (SQLException e) {throw new IllegalStateException("Echec de récupération des formations", e);}finally {JdbcUtils.closeResultSet(resultSet);JdbcUtils.closeStatement(statement);JdbcUtils.closeConnection(connection);}}private boolean isNewSession(Session session, Long idFormation) {return session == null || !session.getIdFormation().equals(idFormation);}
}
public class JdbcSessionDaoTest extends AbstractSessionDaoTest {@Testpublic void shouldFindFormationsWithPlainJdbc() throws Exception {checkSessions( new JdbcSessionDao().findAll() );}
}

La première chose qui saute aux yeux, c'est que le code fonctionnel du DAO est noyé dans le code technique. Sur les 55 lignes de code de cet extrait, seules 21 lignes ont un réel intérêt fonctionnel. La requête est dans une chaîne de caractère Java, c'est un peu gênant quand on veut la copier/coller dans un sqldeveloper par exemple. JDBC n’offre rien de ce côté là, et ce n’est pas son rôle. C’est donc au développeur de gérer une éventuelle externalisation des requêtes. Enfin, les API à manipuler ne sont pas les plus simples (ResultSet contient des dizaines de méthodes). Il n'y a donc rien de particulier à dire sur l'implémentation JDBC, si ce n'est que l'API est assez lourde à utiliser, et que l’utilisation du DriverManager n’est pas recommandée. On peut même identifier que certaines parties du code se répèteront d'un DAO à l'autre. C'est à cette problématique que répond le pattern template.

JdbcTemplate

JdbcTemplate est une classe du framework Spring qui propose de simplifier l'utilisation de JDBC en s'occupant des tâches répétitives. JdbcTemplate s'utilise en conjonction avec d'autres API (dans cet exemple l'interface RowCallbackHandler) et a vocation à s'intégrer d'une manière générale dans spring. Cet exemple a donc été implémenté dans un conteneur Spring et profite des mécanismes d'injection de dépendances.

<bean class="com.zenika.blog.mybatis.impl.jdbctemplate.JdbcTemplateSessionDao" />
public class JdbcTemplateSessionDao implements SessionDao {private static final String FIND_ALL = "select fo.id as id_formation, co.nom as nom_cours, fe.nom as nom_formateur, st.id as id_stagiaire, st.nom as nom_stagiaire " +"from formation fo, cours co, formateur fe, stagiaire st " +"where co.id = fo.id_cours and fe.id = fo.id_formateur and fo.id = st.id_formation " +"order by id_formation";private JdbcTemplate jdbcTemplate;@Overridepublic List<Session> findAll() {SessionRowCallbackHandler handler = new SessionRowCallbackHandler();jdbcTemplate.query(FIND_ALL, handler);return handler.getSessions();}@Autowiredpublic void setDataSource(DataSource dataSource) {this.jdbcTemplate = new JdbcTemplate(dataSource);}
}
class SessionRowCallbackHandler implements RowCallbackHandler {private Session session = null;private List<Session> sessions = new ArrayList<>();public void processRow(ResultSet resultSet) throws SQLException {Long idFormation = resultSet.getLong("id_formation");// pour mapper la session il faut vérifier le changement d'idif ( isNewSession(idFormation) ) {session = new Session();session.setIdFormation(idFormation);session.setNomCours( resultSet.getString("nom_cours") );session.setNomFormateur( resultSet.getString("nom_formateur") );sessions.add(session);}// dans le cas des participants pas besoin de vérifier (d'après la requête)Participant participant = new Participant();participant.setId( resultSet.getLong("id_stagiaire") );participant.setNom( resultSet.getString("nom_stagiaire") );session.getParticipants().add(participant);}private boolean isNewSession(Long idFormation) {return session == null || !session.getIdFormation().equals(idFormation);}List<Session> getSessions() {return sessions;}
}
public class JdbcTemplateSessionDaoTest extends AbstractSessionDaoTest {@AutowiredSessionDao formationDao;@Testpublic void shouldFindFormationsWithJdbcTemplate() throws Exception {checkSessions( formationDao.findAll() );}
}

Premier constat, l'implémentation est découpée en deux classes, et le tout nécessite un peu de configuration. JdbcTemplate encourage une bonne séparation des responsabilités séparant le mapping du reste du traitement (que ce soit dans une classe à part ou dans une classe anonyme). En appliquant le pattern du template, les API de bas niveau ont presque disparu, seul le ResultSet subsiste, de façon quasi-anodine : on n'itère pas manuellement dessus. On ne retrouve donc dans la classe SessionRowCallbackHandler que les 21 lignes à valeur ajoutée de l’implémentation JDBC brute. La requête se trouve toujours dans une constante Java, pas d'amélioration de ce côté là. Un autre changement est visible au niveau du test, celui-ci se fait directement injecter le DAO. Le cycle de vie du DAO est donc géré par Spring tel qu'on lui a déclaré, là où l'implémentation JDBC brute devait instancier elle-même son DAO.

Comme dit précédemment, MyBatis se situe à un niveau d'abstraction entre JdbcTemplate et Hibernate. C'est donc maintenant que je vous propose de voir l'implémentation MyBatis.

MyBatis

Parmi les différents types d'ORM, on trouve les appellations micro et full. MyBatis est un micro ORM, qui se contente de faire le lien entre les mondes relationnel et objet, à la différence des implémentations JPA comme Hibernate qui sont des full ORM. En utilisant JPA, on crée tout un modèle objet à l'image des tables de la base de données (ou vice-versa). MyBatis repose sur la notion de mapper. Le mapper a pour tâche de transformer un résultat de requête en objet. C'est en substance la fonctionnalité principale du framework.

La mise en œuvre de MyBatis nécessite un peu de configuration.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><environments default="test"><environment id="test"><transactionManager type="JDBC" /><dataSource type="POOLED"><property name="driver" value="org.h2.Driver" /><property name="url" value="jdbc:h2:mem:dataSource;DB_CLOSE_DELAY=-1" /><property name="username" value="sa" /></dataSource></environment></environments><mappers><mapper resource="formation-mapper.xml" /></mappers>
</configuration>

On peut ensuite implémenter notre DAO.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.zenika.blog.mybatis.SessionDao"><resultMap id="SessionResultMap" type="com.zenika.blog.mybatis.Session"><id     property="idFormation" column="id_formation" /><result property="nomCours" column="nom_cours" /><result property="nomFormateur" column="nom_formateur" /><collection property="participants" resultMap="ParticipantResultMap" /></resultMap><resultMap id="ParticipantResultMap" type="com.zenika.blog.mybatis.Participant"><id     property="id" column="id_stagiaire" /><result property="nom" column="nom_stagiaire" /></resultMap><select id="findAll" resultMap="SessionResultMap">selectfo.id as id_formation,co.nom as nom_cours,fe.nom as nom_formateur,st.id as id_stagiaire,st.nom as nom_stagiairefrom formation fo, cours co, formateur fe, stagiaire stwhere co.id = fo.id_coursand fe.id = fo.id_formateurand fo.id = st.id_formation</select>
</mapper>
public class MyBatisSessionDaoTest extends AbstractSessionDaoTest {@Testpublic void shouldFindFormationsWithMyBatis() throws Exception {InputStream stream = null;try {stream = Resources.getResourceAsStream("mybatis.xml");SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(stream);SqlSession session = sessionFactory.openSession();checkSessions( session.getMapper(SessionDao.class).findAll() );}finally {IOUtils.closeSilently(stream);}}
}

Si vous êtes attentifs, vous avez remarqué que la classe implémentant SessionDao n'est pas présente. Si vous êtes très attentifs (ou que vous connaissez déjà iBatis/MyBatis) vous aurez compris que c'est MyBatis qui génère l'implémentation de l'interface. La contrepartie, c'est un fichier XML expliquant comment faire le mapping. L'XML est assez verbeux par nature, mais les mappers MyBatis restent tout à fait lisibles. La requête SQL peut être directement copiée sans avoir besoin de retirer des guillemets ou autres points-virgules! Attention par contre aux conflits entre XML et SQL sur des caractères comme < et >. On notera aussi que nous n’avons pas besoin de spécifier de clause ‘’order by’’ dans la requête, car c’est la balise <id> qui déterminera le passage à un nouvel objet. La configuration nous montre aussi qu'il existe une notion d'environnement dans MyBatis, et qu'il est capable de gérer un pool de connexion. C'est une des nombreuses fonctionnalités du framework en plus du simple mapping. Seul le test gagne en complexité, on a besoin de créer une SqlSessionFactory (une seule fois) et d'ouvrir une session afin de récupérer le Dao. Dans un conteneur comme spring, la question ne se pose pas : on peut directement se faire injecter les mappers où c'est nécessaire. Il ne reste donc plus qu'à voir l'implémentation avec Hibernate.

Hibernate

Je ne suis pas certain qu'il soit nécessaire de présenter Hibernate. C'est un full ORM qui nécessite donc de déclarer des entités correspondant aux tables de la base de données. Mon cas d'utilisation a volontairement été choisi pour afficher une des limitation des full ORM. Cela suppose donc de passer par des objets intermédiaires pour instancier les objets en sortie du DAO. Il est bien entendu possible en JPQL d'instancier directement une classe n'étant pas une entité, mais cela n'enlève pas la nécessité d'avoir des entités.

@Entity
@Table(name="formation")
public class Formation {@Id@Column(name="id")private Long id;@ManyToOne@JoinColumn(name="id_cours")private Cours cours;@ManyToOne@JoinColumn(name="id_formateur")private Formateur formateur;@Column(name="date_debut")private Date dateDebut;@OneToMany@JoinColumn(name="id_formation")private Set<Stagiaire> stagiaires;...
}

Dans cet exemple nous avons besoin de créer quatre entités ne serait-ce que pour assurer le mapping. Il est faut aussi ajouter les champs que nous n'utilisons pas. On peut s'en passer pour de la lecture, mais une insertion ne fonctionnera pas si une colonne NOT NULL n'est pas mappée. Nous avons donc nos quatre entités, voyons maintenant l'implémentation.

On utilise Hibernate en tant qu'implémentation de JPA, il faut donc créer un fichier persistence.xml.

<persistence
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0"><persistence-unit name="test"><provider>org.hibernate.ejb.HibernatePersistence</provider><class>com.zenika.blog.mybatis.impl.jpa.Cours</class><class>com.zenika.blog.mybatis.impl.jpa.Formateur</class><class>com.zenika.blog.mybatis.impl.jpa.Formation</class><class>com.zenika.blog.mybatis.impl.jpa.Stagiaire</class><properties><property name="hibernate.connection.url" value="jdbc:h2:mem:dataSource;DB_CLOSE_DELAY=-1" /><property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"></property><property name="hibernate.connection.driver_class" value="org.h2.Driver" /><property name="hibernate.connection.username" value="sa" /></properties></persistence-unit>
</persistence>

On peut ensuite implémenter le DAO.

public class HibernateSessionDao implements SessionDao {private EntityManager entityManager;public HibernateSessionDao(EntityManager entityManager) {this.entityManager = entityManager;}@Overridepublic List<Session> findAll() {@SuppressWarnings("unchecked")List<Formation> formations = entityManager.createQuery("from Formation").getResultList();List<Session> sessions = new ArrayList<>();for (Formation formation : formations) {Session session = new Session();session.setIdFormation( formation.getId() );session.setNomCours( formation.getCours().getNom() );session.setNomFormateur( formation.getFormateur().getNom() );for (Stagiaire stagiaire : formation.getStagiaires()) {Participant participant = new Participant();participant.setId( stagiaire.getId() );participant.setNom( stagiaire.getNom() );session.getParticipants().add(participant);}sessions.add(session);}return sessions;}
}
public class HibernateSessionDaoTest extends AbstractSessionDaoTest {@Testpublic void shouldFindFormationsWithHibernate() throws Exception {EntityManager entityManager = Persistence.createEntityManagerFactory("test").createEntityManager();checkSessions( new HibernateSessionDao(entityManager).findAll() );}
}

Sans surprise, le code n'est pas compliqué. Il s'agit d'une simple transformation d'objets Formation en objets Session. Comme pour le test MyBatis, il est nécessaire de créer un EntityManager, mais JPA reposant sur des conventions, le code est beaucoup moins verbeux. Il ne serait bien entendu pas nécessaire d'écrire ce code dans un conteneur Spring, EJB ou autre... Il ne faut par contre pas perdre de vue qu'il a été nécessaire de créer quatre classes afin de pouvoir implémenter le DAO ce qui fait de cette solution la plus verbeuse. D'une manière générale, ce genre de solution pousse souvent au compromis, au détriment d'une solution plus naturelle.

Attention aussi à ne pas reproduire cet exemple dans vos projets. Afin de simplifier au maximum cet exemple, Hibernate se retrouve à faire 1+3n requêtes à cause des relations. Il faut donc préciser qu’on veut charger les relations en ‘’eager loading’’ soit par annotations, ou dans la requête JPQL. Il n’est pas non plus obligatoire de passer par les entités. On peut passer par l’opérateur ‘’new’’ en JPQL pour passer directement les valeurs par le constructeur. Par contre un des postulats de départ était que les classes ‘’Session’’ et ‘’Participant’’ ne sont pas sous notre contrôle. On ne peut donc pas ajouter de constructeur. De son côté MyBatis, peut travailler aussi bien avec des setters qu’avec des constructeurs, et on peut même combiner les deux. Hibernate propose également un système de ‘’ResultTransformer’’ qui répond au besoin de mapper les résultats en direct sans passer par les entités. Cette solution est cependant spécifique à Hibernate et ne s’applique donc pas à JPA.

Pour conclure

MyBatis est un ORM très souple. En prenant le parti de se concentrer sur le mapping de résultats, MyBatis nous laisse maîtres du modèle de données sous-jacent. Il tolère de fait un changement dans la structure de la base de données, si tant est que les nouvelles requêtes renvoient les mêmes résultats. Les DBA peuvent directement modifier les requêtes avec les développeurs et n'ont pas besoin d'apprendre un langage supplémentaire (et je n'ai jamais vu de DBA optimiser une requête JPQL/Criteria!).

MyBatis offre également une meilleure abstraction :

  • puisqu'on ne manipule pas de JDBC dans la grande majorité des cas
  • que le mapping est homogène, que nos classes soient mappées ou non sur nos tables
  • qu’il offre une maîtrise du SQL exécuté
  • et qu’il écarte les pièges courants liés à une mauvaise utilisation d’un full ORM

Zenika propose désormais une formation MyBatis si vous êtes intéressé par ce framework de persistance riche et souple. A l'issue des deux jours vous connaîtrez tout le nécessaire pour bien utiliser MyBatis. La courbe d’apprentissage de ce framework est bien plus douce que celles de JDBC ou JPA, et vous garantira d’être opérationnel très rapidement.

Les sources de l'article sont téléchargeables ici.

Hibernate 多表映射(Mybatis,Springjdbc,Hibernate ,JDBC的多表映射学习比较)相关推荐

  1. mybatis和hibernate的对比总结

    mybatis和hibernate 第一步, 首先让我们对mybatis和hibernate对比了解下 1. Hibernate :Hibernate 是当前非常流行的ORM框架,对数据库结构提供了较 ...

  2. Mybatis解决jdbc编程的问题以及mybatis与hibernate的不同

    Mybatis解决jdbc编程的问题: 1. 数据库连接创建.释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题. 解决:在SqlMapConfig.xml中配置数据连接池,使 ...

  3. 数据持久化框架为什么放弃Hibernate、JPA、Mybatis,最终选择JDBCTemplate!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框 ...

  4. MyBatis 与 Hibernate

    MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不用再花费精力去处理诸如注册驱动.创建 Connection.配置 Statem ...

  5. jooq实体 和mysql_几个数据持久化框架Hibernate、JPA、Mybatis、JOOQ的比较

    因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template. Hibernate ...

  6. MyBatis与Hibernate比较

    MyBatis: 1.是一个sql语句映射的框架(工具). 2.注重pojo与sql之间的映射关系.不会为程序员在运行期自动生成sql 3.自动化程度低,手工映射sql灵活程度高 4.需要开发人员熟练 ...

  7. struts、hibernate、spring、 mybatis、 spring boot 等面试题汇总

    1.谈谈你对Struts的理解. 答: 1. struts是一个按MVC模式设计的Web层框架,其实它就是一个大大的servlet,这个Servlet名为ActionServlet,或是ActionS ...

  8. 后端技术:数据持久化框架为什么放弃 Hibernate、JPA、Mybatis,最终选择 JDBCTemplate!...

    因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template. Hibernate ...

  9. 后端开发:数据持久化框架为什么放弃Hibernate、JPA、Mybatis,最终选择JDBCTemplate!...

    因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template. Hibernate ...

最新文章

  1. 右边补0 润乾报表_制作按奖金分段统计的员工业绩报表
  2. android 一周日历,(Android)获取一周的第一天
  3. Linux内存管理 - 页表的映射过程初步了解
  4. 局部静态变量Static详解
  5. 阿里P8亲自讲解!java中级开发工程师需要掌握的技能
  6. 安装rlwrap 的简单方法
  7. Socket编程实践(2) --Socket编程导引
  8. JavaScript之事件委托(附原生js和jQuery代码)
  9. python通过SNMP协议收集服务器监控信息(安装、配置、示例)
  10. DW8里面的HTML面板在哪里,打开Dreamweaver8窗口后,如果没有出现属性面板,可执行()菜单中的 - 问答库...
  11. Keras Model AttributeError:’str‘ object has no attribute ’call‘
  12. input换行输入_小白也能学的Python基础语法-变量与输入和输出
  13. SSIS 抽取Excel每个sheet页的数据
  14. 傅里叶变换和拉普拉斯变换公式总结
  15. 基于MATLAB印刷体汉字识别解析
  16. Win10 锁屏自动息屏解决方案
  17. linux apache 查看mpm 配置方式,apache httpd mpm配置
  18. 月之数 HDU2502
  19. epub是什么文件?epub文件怎么打开?
  20. 整理的apollo 入门课程

热门文章

  1. 生而为人,请务必善良
  2. 《孙子兵法作战指挥之兵势篇》
  3. 服务器访问时502 Server dropped connection 错误解决方法
  4. java开源办公OA项目:通过极光SDK获取设备号绑定到用户属性
  5. 吴军:优秀的人,都有一些相似之处
  6. 在华为云 CCE 上部署 EMQX MQTT 服务器集群
  7. 消息Hander dispatchfalled; nested excepton is java.ang.NOSuchMethodError: org.springramewrk.utl.tring
  8. 吴恩达《机器学习系列课程》学习笔记(一)
  9. Tableau表计算(2):计算依据
  10. c语言中完美立方的程序,完美立方,完全立方公式