Mybatis注解开发指北


目录

文章目录

  • Mybatis注解开发指北
    • @[toc]
    • 0. Mybatis注解开发步骤
    • 1. 导入相关配置文件
    • 2. 配置数据库连接
    • 3. 创建数据库对应的实体类(entity)
    • 4. 创建实体类对应的Dao/Mapper实现增删改查
      • 4.1 Select查询
        • 4.1.1 立即加载EAGER和延迟加载LAZY
      • 4.2 Insert插入
      • 4.3Update更新
      • 4.4Delete删除
    • 5 创建相应的测试类
      • 5.1 使用junit创建测试类

0. Mybatis注解开发步骤

  1. 导入Mybatis、Mysql及相关辅助包
  2. 配置数据库连接
  3. 创建数据库对应的实体类(entity)
  4. 创建实体类对应的Dao/Mapper实现增删改查
  5. 创建相应的测试类

1. 导入相关配置文件

  • 用maven创建工程,在pom.xml中的dependencies中导入如下代码:
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.3</version>
</dependency>
  • 同时可以导入mysql、log4j和junit用来测试和输出文档测试
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.12</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version>
</dependency>
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope>
</dependency>
  • 在resources下新建log4j.properties文件将以下内容拷贝进去,用于输出日志等相关配置。
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=/axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

2. 配置数据库连接

  • 配置数据库连接是为了连接数据库以便进行CRUD操作

  • 配置数据库连接,在resources文件夹中新建SqlMapConfig.xml储存数据库相关配置,配置如下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis主配置文件-->
<configuration>
<!--    配置类型别名--><typeAliases><package name="com.database.mybatis.entity"/></typeAliases><!--    配置环境--><environments default="mysql"><environment id="mysql">
<!--            事务类型--><transactionManager type="JDBC"></transactionManager>
<!--            配置数据源--><dataSource type="POOLED">
<!--            配置连接数据库的基本信息--><!--Mysql 6.0之前driver   驱动写com.mysql.jdbc.Driver--><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/test"/><property name="username" value="root"/><property name="password" value="qwer123"/></dataSource></environment></environments><!--    指定dao接口所在位置--><mappers><package name="com.database.mybatis.dao"></package></mappers>
</configuration>
  • 类型别名的作用:

    • 类型别名的作用是为Java类型折翼短的名字,至于xml配置有关,可以减少类完全限定名的冗余。单个配置的时候可以是:
    <typeAliases><typeAlias alias="Student" type="com.database.mybatis.entity.Student"/>
    </typeAliases>
    
    • 也可以进行批量配置,在如下方法中配置别名,Mybatis可以在entity下搜索需要的Java Bean的瘦子米小写的非限定类名作为他的别名,比如com.database.mybatis.entity.Student的别名是student
    <typeAliases><package name="com.database.mybatis.entity"/>
    </typeAliases>
    

3. 创建数据库对应的实体类(entity)

  • 创建实体类的作用是为了将存放数据库中返回的数据,以便于进行相应的操作。

  • 创建实体类之前应先建立数据库相应的表,提供测试的数据库表有3个:Teacher、Class、Student。三个表是多对多的关系,EER图如下:

  • sql语句如下:
CREATE DATABASE test;use test;CREATE TABLE student(id INT PRIMARY KEY,teacher_id INT,class_id INT,student_nub BIGINT NOT NULL,student_name VARCHAR(20) NOT NULL,student_sex VARCHAR(20) DEFAULT '男',student_phone VARCHAR(20)
);SELECT * FROM student;INSERT INTO student VALUES(1,1,1,1800300101,'一班一号','男',NULL);CREATE TABLE class(id INT PRIMARY KEY,teacher_id INT,student_id INT,class_nub BIGINT NOT NULL,class_name VARCHAR(20) NOT NULL,class_maxperson INT NOT NULL,class_haveperson INT NOT NULL,class_place INT NOT NULL
);INSERT INTO class VALUES(1,1,1,300101,'三院一年级一号课',80,0,17101);SELECT * FROM class;CREATE TABLE teacher(id INT PRIMARY KEY,student_id INT,class_id INT,teacher_nub BIGINT NOT NULL,teacher_name VARCHAR(20) NOT NULL,teacher_sex VARCHAR(20) DEFAULT '男',teacher_phone VARCHAR(20)
);INSERT INTO teacher VALUES(1,1,1,003001,'三院一号教师','男','13113311331');SELECT * FROM teacher;alter table student add foreign key(class_id) references class(id);
alter table student add foreign key(teacher_id) references teacher(id);
alter table teacher add foreign key(class_id) references class(id);
alter table teacher add foreign key(student_id) references student(id);
alter table class add foreign key(teacher_id) references teacher(id);
alter table class add foreign key(student_id) references student(id);
  • 创建实体类一般放入entiy包下,目录结构如下:

  • 实体类创建时尽量遵循阿里巴巴java开发手册命名规则,养成良好习惯。数据库中表的字段类型需要与Java类型相对应,Java变量名以表字段名全称命名,例如表字段为student_id,则Java变量名应为StudentId。

  • sql变量类型与Java变量对照表如下:

Java类型 SQL类型
boolean BIT
byte TINYINT
short SMALLINT
int INTEGER
long BIGINT
String CHAR,VARCHAR,LONGVARCHAR
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP
  • Class实体类如下:
public class Class implements Serializable {private Integer id;private Integer teacherId;private Integer studentId;private BigInteger classNub;private String className;private Integer classMaxPerson;private Integer classHavePerson;private Integer classPlace;
}
  • 然后创建实体类对应的getter、setter和toString。完整的Class实体类如下:
package com.database.mybatis.entity;import java.io.Serializable;
import java.math.BigInteger;
import java.util.List;/*** @author linxi* @function* @project database* @package com.database.mybatis.model* @date 2020/5/8-1:37 下午*/
public class Class implements Serializable {private Integer id;private Integer teacherId;private Integer studentId;private BigInteger classNub;private String className;private Integer classMaxPerson;private Integer classHavePerson;private Integer classPlace;@Overridepublic String toString() {return "Class{" +"id=" + id +", teacher_id=" + teacherId +", student_id=" + studentId +", class_nub=" + classNub +", class_name='" + className + '\'' +", class_maxperson=" + classMaxPerson +", class_haveperson=" + classHavePerson +", class_place=" + classPlace +'}';}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public Integer getTeacherId() {return teacherId;}public void setTeacherId(Integer teacherId) {this.teacherId = teacherId;}public Integer getStudentId() {return studentId;}public void setStudentId(Integer studentId) {this.studentId = studentId;}public BigInteger getClassNub() {return classNub;}public void setClassNub(BigInteger classNub) {this.classNub = classNub;}public String getClassName() {return className;}public void setClassName(String className) {this.className = className;}public Integer getClassMaxPerson() {return classMaxPerson;}public void setClassMaxPerson(Integer classMaxPerson) {this.classMaxPerson = classMaxPerson;}public Integer getClassHavePerson() {return classHavePerson;}public void setClassHavePerson(Integer classHavePerson) {this.classHavePerson = classHavePerson;}public Integer getClassPlace() {return classPlace;}public void setClassPlace(Integer classPlace) {this.classPlace = classPlace;}
}

4. 创建实体类对应的Dao/Mapper实现增删改查

  • Dao和Mapper在注解开发中已经没有明显区别,所以不作区分。在Mybatis注解开发中,只要定义好对应的接口并加上合适的注解即可直接使用,不需要再编写Dao的具体实现类。
  • 因为我们实体类中的变量名与数据库表中的字段名不一致,所以我们需要任意一个用@Results注解使变量名与字段名进行统一,在再其他借口上使用@Resultmap()调用统一关系,代码如下。
/*** results的ID用于定义此对应的名称,在其他接口中直接使用@Resultmap("ID")来使用此对应关系,无需重写。* 表中主键需要在@Result()中的id属性设为true(默认为false)* @resylt() 的column属性为表中字段名,property为实体类变量名*/
@Results(id = "classDao", value = {@Result(id = true,column = "id",property = "id"),@Result(column = "teacher_id",property = "teacherId"),@Result(column = "student_id",property = "studentId"),@Result(column = "class_nub",property = "classNub"),@Result(column = "class_name",property = "className"),@Result(column = "class_maxperson",property = "classMaxPerson"),@Result(column = "class_haveperson",property = "classHavePerson"),@Result(column = "class_place",property = "classPlace")
})

4.1 Select查询

  • 接口例子:

    • 查询Class所有内容,返回值为Class类型的List
    /*** results的ID用于定义此对应的名称,在其他接口中直接使用@Resultmap("ID")来使用此对应关系,无需重写。* 表中主键需要在@Result()中的id属性设为true(默认为false)* @resylt() 的column属性为表中字段名,property为实体类变量名*/@Results(id = "classDao", value = {@Result(id = true,column = "id",property = "id"),@Result(column = "teacher_id",property = "teacherId"),@Result(column = "student_id",property = "studentId"),@Result(column = "class_nub",property = "classNub"),@Result(column = "class_name",property = "className"),@Result(column = "class_maxperson",property = "classMaxPerson"),@Result(column = "class_haveperson",property = "classHavePerson"),@Result(column = "class_place",property = "classPlace")})
    @Select("select * from class")
    List<Class> findAllClass();
    
    • 通过id查询
    @ResultMap("classDao")
    @Select("select * from class where id=#{id}")
    List<Class> findClassById(Integer classId);
    
    • 模糊查询

      • 方式一:
      @Select("select * from class where class_name like '%${value}%'")
      

      ​ 这种方式直接传送参数进来就可,不需要加%号。例如传送张三作为value即相当于:select * from class where class_name like %张三%。\

      例子:

      @ResultMap("classDao")
      @Select("select * from class where class_name like '%${value}%'")
      List<Class> findClassByClassName(String className);
      
      • 方式二:
      @Select("select * from class where class_name like #{username}")
      

      这种方式传送参数时需要带%,例如传送%张三%作为value即相当于:select * from class where class_name like %张三%。

      例子:

              @ResultMap("classDao")@Select("select * from class where class_name like #{username}")List<Class> findClassByClassName(String className);
      
    • 多表查询:多表对应关系有两种:一对一和一对多。

      • 一对一:假设有表A,B;A表中有字段id和b_id,b_id对应B表的id(id为唯一值),所以A表的每一列都有唯一一个B与其对应,则此关系为一对一。一对一关系需要用@one注解映射。

        • 假设查询某节课及其对应的老师,因为每个课程只有一个老师,所以关系为一对一。
        • 首先需要在Class实体类中建立Teacher类型变量用于存储老师信息,并建立相应的gettter和setter。
        private Teacher teacher;public Teacher getTeacher() {return teacher;
        }public void setTeacher(Teacher teacher) {this.teacher = teacher;
        }
        
        • 第二步,需要在TeacherDao中建立通过classID查询teacher的接口。
        @Select("select * from teacher where class_id=#{class_id}")
        Teacher findTeacherByClassId(Integer classId);
        
        • 第三步,在ClassDao中建立通过classId查询课程及其对应老师的接口。实质上是查询class但是class实体类中的Teacher中要包含课程对应的老师。多表查询的@one注解是属于@result的参数。

        @Results(value = {@Result(id = true, column = "id", property = "id"),@Result(column = "teacher_id", property = "teacherId"),@Result(column = "student_id", property = "studentId"),@Result(column = "class_nub", property = "classNub"),@Result(column = "class_name", property = "className"),@Result(column = "class_maxperson", property = "classMaxPerson"),@Result(column = "class_haveperson", property = "classHavePerson"),@Result(column = "class_place", property = "classPlace"),@Result(property = "teacher", column = "teacherId",one = @One(select ="com.database.mybatis.dao.TeacherDao.findTeacherByClassId",fetchType = FetchType.EAGER))
        })
        @Select("select * from class where id = #{id}")
        Class findClassAndTeacherByClassId(Integer classId);
        
      • 一对多:假设有表A,B;A表中有字段id和b_room,b_room对应B表的room(room不唯一),所以A表的每一列都有多个B与其对应,则此关系为一对多。一对多关系需要用@many注解映射

        • 假设查询上某节课的所有学生,因为一个课程有多名学生,所以关系为一对多。
        • 首先需要在Class实体类中建立List类型变量用于存储老师信息,并建立相应的gettter和setter。
        private List<Student> students;public List<Student> getStudents() {return students;
        }public void setStudents(List<Student> students) {this.students = students;
        }
        
        • 第二步,需要在StudentDao中建立通过classID查询student的接口。
        @Select("select * from student where class_id=#{class_id}")
        List<Student> findStudentByClassId(Integer classId);
        
        • 第三步,在ClassDao中建立通过classId查询课程及其对应学生的接口。实质上是查询class但是class实体类中List中要包含课程对应的学生。多表查询的@many注解是属于@result的参数。
        @Select("select * from class where id=#{id}")
        @Results(value = {@Result(id = true,column = "id",property = "id"),@Result(column = "teacher_id",property = "teacherId"),@Result(column = "student_id",property = "studentId"),@Result(column = "class_nub",property = "classNub"),@Result(column = "class_name",property = "className"),@Result(column = "class_maxperson",property = "classMaxPerson"),@Result(column = "class_haveperson",property = "classHavePerson"),@Result(column = "class_place",property = "classPlace"),@Result(property = "students", column = "id",many = @Many(select ="com.database.mybatis.dao.StudentDao.findStudentByClassId",fetchType = FetchType.LAZY))
        })
        List<Class> findClassAndStudentsByClassId(Integer classId);
        

4.1.1 立即加载EAGER和延迟加载LAZY

  • 延迟加载和立即加载的区别:

    • 顾名思义,立即加载就是在sql语句执行的时候直接从数据库取出数据,而延迟加载就是虽然sql语句执行,但是只有在用到相应的数据的时候才从数据库加载出来。
  • 什么时候用延迟加载和立即加载:
    • 延迟加载多用于查询一对多的时候,立即加载多用于查询一对一的时候,但并不绝对。
  • 为什么要用延迟加载:
    • 使用延迟加载的目的是为了减少系统资源的消耗,例如查询课程和选择此课程的学生的时候,当一个课程中只有几十个学生的时候,我们使用延迟加载和立即加载对系统资源的消耗并不明显,但是当一个课程有几千万个学生的时候,同时我们并不是立刻使用所有学生的信息。我们如果使用立即加载就会造成大量浪费系统资源,此时使用延迟加载的随用随取模式就会有很大的优势。

4.2 Insert插入

  • 插入数据到表中要使用@Insert注解

例子:

@ResultMap("classDao")
@Insert("insert into class(id,teacher_id,student_id,class_nub,class_name,class_maxperson,class_haveperson,class_place) values(#{id},#{teacher_id},#{student_id},#{class_nub},#{class_name},#{class_maxperson},#{class_haveperson},#{class_place})")
void insertClass(Class aClass);

此中的所有字段都在Class实体类中,只要在#{}中填入对应的字段名或者别名并将Class作为参数传入,Mybatis就会在Class中自动查找对应的关系。


4.3Update更新

  • 更新数据到表中要使用@Update注解

例子:

@ResultMap("classDao")
@Update("update class set teacher_id=#{teacher_id},student_id=#{student_id},class_nub=#{class_nub},class_name=#{class_name},class_maxperson=#{class_maxperson},class_haveperson=#{class_haveperson},class_place=#{class_place} where id=#{id}")
void updateClassById(Class aClass);

4.4Delete删除

  • 从表中删除数据使用@Delete注解

例子:

@ResultMap("classDao")
@Delete("delete from class where id=#{id}")
void deleteClassById(Integer classId);

5 创建相应的测试类

  • text.java下创建测试类,测试类的目录结构最好与项目结构相同。所以我们在text.java.com.database.mybatis下创建测试类testClass.java

  • 想要能够使用我们创建好的接口操作数据库需要如下几个步骤:

    1. 读取数据库配置文件:用于连接数据库及其相关配置
    2. 使用建造者模式创建SqlSessionFactory工厂:使用建造者模式可以隐藏创建工厂的细节,直接将我们需要的工厂的“图纸”传入即可建造出我们想要的工厂。
    3. 使用工厂模式创建SqlSession对象:使用工厂模式创建SqlSession对象可以解耦,可以在不改变源码的方式进行操作
    4. 使用SqlSession创建Dao接口的代理对象执行方法:在不修改源码的基础上对已有方法增强,可是已实现不写Dao的实现类,就可以实现功能。
    5. 释放资源

例子:

//读取数据库配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//建造者模式创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//使用工厂模式生产SqlSession对象
SqlSession session = factory.openSession();
//使用SqlSession创建Dao接口的代理对象执行方法
ClassDao classDao = session.getMapper(ClassDao.class);
//使用代理对象执行方法
List<Class> aClass = classDao.findAll();
for (Class aClass1 : aClass) {System.out.println(aClass1);System.out.println(aClass1.getStudents());
}
  • 但是在实际生产环境中我们可以更简单的进行操作:
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml").getMapper(ClassDao.class);List<Class> aClass = classDao.findAll();
for (Class aClass1 : aClass) {System.out.println(aClass1);System.out.println(aClass1.getStudents());
}

5.1 使用junit创建测试类

  • 创建TestClass.java
  • 在进行测试前我们需要进行上述配置拿到Dao的代理对象
public class TestClass {private InputStream in;private SqlSessionFactory factory;private SqlSession session;private ClassDao classDao;@Beforepublic void init() throws IOException {in = Resources.getResourceAsStream("SqlMapConfig.xml");factory = new SqlSessionFactoryBuilder().build(in);session = factory.openSession();classDao = session.getMapper(ClassDao.class);}
  • 在测试结束后我们需要释放资源,所以增加如下方法:
@After
public void destroy() throws IOException {session.commit();session.close();in.close();
}
  • 以通过id查询课程为例创建测试类。

    1. 创建testFindClassById方法。
    2. 通过代理对象调用findClassById方法并传入参数。
    3. 打印查询结果。
@Test
public void testFindClassById(){List<Class> classes = classDao.findClassById(1);for (Class aClass : classes) {System.out.println(aClass);}
}

Mybatis注解开发指北相关推荐

  1. MyBatis-学习笔记12【12.Mybatis注解开发】

    Java后端 学习路线 笔记汇总表[黑马程序员] MyBatis-学习笔记01[01.Mybatis课程介绍及环境搭建][day01] MyBatis-学习笔记02[02.Mybatis入门案例] M ...

  2. Mybatis注解开发笔记

    Mybatis注解开发(笔记) 欢迎来到菜鸟研究所 创建新的Maven项目 配置文件 prom.xml log4j.properties jdbcConfig.properties SqlMapCom ...

  3. Mybatis注解开发(超详细)

    Mybatis注解开发 mybatis的常用注解 使用 Mybatis 注解实现基本 CRUD 项目目录结构 编写实体类 使用注解方式开发持久层接口 编写 SqlMapConfig.xml 配置文件 ...

  4. Mybatis 注解开发 + 动态SQL

    Hello 大家好我是橙子同学,今天分享注解Mybatis注解开发+动态sql 目录 每文一铺垫(今天有小插曲哦) 注解开发 添加 @Insert 删除 @Delete 查询 @Select 修改 @ ...

  5. Mybatis注解开发(一对一)

    其他代码访问:Mybatis注解开发基础操作 1.添加OrderMapper接口 public interface OrderMapper {// @Select("select *,o.i ...

  6. java day56【 Mybatis 延迟加载策略 、 Mybatis 缓存、Mybatis 注解开发 】

    第1章 Mybatis 延迟加载策略 1.1 何为延迟加载? 1.2 实现需求 1.3 使用 assocation 实现延迟加载 1.3.1 账户的持久层 DAO 接口 1.3.2 账户的持久层映射文 ...

  7. Mybatis注解开发出现Type interface Mapper.StudentMapper is not known to the MapperRegistry异常解决办法

    Mybatis注解开发出现Type interface Mapper.StudentMapper is not known to the MapperRegistry异常解决办法 在核心配置文件中,配 ...

  8. mybatis注解开发动态sql

    mybatis注解开发动态sql 本篇来讲一下如何使用mybatis注解模式中的动态sql 先来讲一下什么是动态sql 在我们实际开发的时候可能会出现很多方法需要一条很相似的sql语句来进行增删改查, ...

  9. (软件工程)GPU并行软件开发指北

    事先声明,本篇博文中不涉及任何技术,原理和代码.如果想知道GPU开发实践方法,请直接查阅OpenCL与CUDA开发手册. 在"GPU并行软件开发指北"这篇博文中,我仅仅是想谈论一下 ...

最新文章

  1. python中label组件参数_Python tkinter(六) 标签(Label)组件的属性说明及示例
  2. python语言学了有用吗-转行学习Python开发有什么优势
  3. Java 14 发布了,再也不怕 NullPointerException 了!
  4. Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.
  5. Android:Eclipse+ADT+Android SDK 搭建安卓开发环境
  6. php object 对象不存在。增加对象_《相亲者女》:找一个匹配的对象,但永远不存在...
  7. java实现串口通信 485协议
  8. 如何理解原码一位乘法的计算过程
  9. java的第十一章总结(枚举,泛型)
  10. JavaScript系列之条件运算符
  11. Multipass中文文档-教程
  12. 云端守望者(下):十八般武艺
  13. 【OpenCV+Qt】实现简易视频播放器——支持进度条拖动
  14. QT项目练习--砍多多校园二手交易平台(C++)
  15. 网上酒店客房预定系统数据库设计
  16. CAD二次开发 关于BlockTable、BlockTableRecord和BlockReference的一丢丢理解
  17. 小吴的《机器学习 周志华》学习笔记 第一章 绪论
  18. 论文阅读_音频压缩_Encodec
  19. 淘沙河 - 清凉解暑之电风扇
  20. graph embedding 论文及源码阅读 deepwalk line node2vec bine

热门文章

  1. 2020-11-12杂谈
  2. HDTV 之-HDMI HPD
  3. ue4 点击某一物体触发事件_UE4引擎——姜小白修炼记(三)
  4. 深度终端:ubuntu等linux下好用的远程终端软件
  5. 手机rar压缩包密码忘了怎么办,rar压缩包不能复制打印、rar压缩包忘记密码怎么办?
  6. 新坑首发《每晚一个恐怖的IT技术学习小故事》让我们一边学习技术,一边体验恐惧吧~
  7. ant design DatePicker时间组件 本地中文 发布后变成英文
  8. 个人总结---微信抢票应用
  9. 计算机的配件知识,最基本的入门知识:电脑由哪些部件组成?
  10. 工作9年的程序员几点感受