MyBatis持久层框架
数据持久化:将程序的数据在持久状态和瞬时状态转化的过程
官方文档: MyBatis中文网
内存:断电即失
第一个程序
TeacherMapper接口,有一个获取所有Teacher的方法待实现
public interface TeacherMapper {
List<Teacher> getTeacherList();
}
- 导入依赖(操作数据库时,还需要导入一个mysql-connector-java的依赖)
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
- mybatis.xml文件连接数据库
jdbc.properties
driver=com.mysql.cj.jdbc.Driver
username=root
password=root
url=jdbc:mysql:///mybatis?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC
官方配置文件模板
<?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="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
-
原本的dao接口实现类daoImpl,现在变成一个和接口同名的mapper.xml文件
TeacherMapper.xml,就是把这里面的configuration全部换成mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace写入完全限定名,表明这是哪个Mapper接口的实现类-->
<mapper namespace="com.changGe.li.mapper.TeacherMapper">
<!--查询方法,id要和接口中的方法名相同,
resultType写上返回值类型-->
<select id="getTeacherList" resultType="com.changGe.li.pojo.Teacher">
select * from teacher;
</select>
</mapper>
- mybatis-config.xml配置文件中注册TeacherMapper的实现类(TeacherMapper.xml)
mybatis-config.xml
注意:文件用/号来分隔,类用.号来分隔
<?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>
<!--读取配置文件-->
<properties resource="jdbc.properties"></properties>
<!--给类型起别名,以后其它文件再引用时,就可以直接使用别名了-->
<typeAliases>
<typeAlias type="com.changGe.li.pojo.Teacher" alias="teacher"/>
</typeAliases>
<!-- 环境 -->
<environments default="development">
<environment id="development">
<!-- 事务管理器类型为jdbc 默认手动提交事务-->
<transactionManager type="JDBC"/>
<!-- 连接池状态是POOLED(连接) -->
<dataSource type="POOLED">
<!--#{driver}从配置文件中读取driver的值-->
<!-- $是Statement模式读取,#是PreparedStatement模式,但是值会变成字符串格式
多数情况下用#,但是遇到如分页的情况时:必须用$
-->
<!--这里如果用#,最后的值会变成'#{username}'-->
<property name="driver" value="${driver}"/>
<property name="url" value="jdbc:mysql:///mybatis?useUnicode=true&ampcharacterEncoding=utf8"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--必须注册mapper,不然报错,就是让MyBatis知道应该去哪里找mapper文件
注意:文件用/号来分隔,类用.号来分隔
-->
<mappers>
<mapper resource="com/changGe/li/mapper/StudentMapper.xml"/>
<mapper resource="com/changGe/li/mapper/TeacherMapper.xml"/>
</mappers>
</configuration>
xml中的&要加上amp;来表示
<property name="url" value="jdbc:mysql:///mybatis?useUnicode=true&ampcharacterEncoding=utf8"/>
- Mybatis工具类,获取sqlSession(数据库操作)对象
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
//读取配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-onfig.xml");
//工厂构建类 构建 得到sql会话工厂
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//用工厂类来获取执行sql操作的对象
public static SqlSession getSqlSession(){
//打开会话,也就是开启和数据库的连接
return sqlSessionFactory.openSession();
}
}
- 用SqlSession对象来获取实现类对象,然后执行方法
@Test
public void test(){
//工具类获取SqlSession对象
SqlSession sqlSession = MyBatisUtil.getSqlSession();
//底层用反射机制,获取了接口TeacherMapper的类对象
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
//接口被实现类TeacherMapper.xml实现了,调用接口就是调用实现类的方法
List<Teacher> teacherList = mapper.getTeacherList();
id:1,姓名是:秦老师
for (Teacher teacher : teacherList) {
System.out.println("id:"+teacher.getId()+",姓名是:"+teacher.getName());
}
//一定要关闭会话,免得占用资源
sqlSession.close();
}
- 早年间官方给我们提供了一些默认的方法,可以通过传入方法路径,来直接执行.不过没有第一种方法好理解
@Test
public void test(){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
//sql会话时,直接调用接口中的方法
List<Teacher> teacherList = sqlSession.selectList("com.changGe.li.mapper.TeacherMapper.getTeacherList");
//id:1,姓名是:秦老师
for (Teacher teacher : teacherList) {
System.out.println("id:"+teacher.getId()+",姓名是:"+teacher.getName());
}
sqlSession.close();
}
注意事项
如果找不到对象,可能因为maven是找不到mapper.xml,用下面的代码,让maven在构建项目时把文件构建进来
<build>
<resources>
<resource>
<!--将src,main,java下的.properties和.xml文件,可以被maven发现-->
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
好像说mapper.xml中的中文注释也会出问题,可能是要把文件格式改成UTF-8
官方名词概念
三大对象
在后面的生命周期和作用域会讲解
完全限定名
就是可以让mybatis直接打到资源的一个精确名字,如com.changGe.li.pojo.Teacher
CRUD
对象中的值可以直接拿来用
<!-- 从parameterType中拿到的参数,可以直接作用于语句中 -->
<select id="getTeacherByIdAndName" resultType="teacher" parameterType="map">
select * from teacher where id=#{id} and name = #{name};
</select>
@Test
public void test(){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
HashMap<String, String> map = new HashMap<String, String>();
map.put("id","1");
map.put("name","秦老师");
Teacher teacherByIdAndName = mapper.getTeacherByIdAndName(map);
System.out.println(teacherByIdAndName);
sqlSession.close();
}
增删改一定要提交事务sqlsession.commit();
<insert id="addTeacher" parameterType="teacher">
insert into teacher values(#{id},#{name})
</insert>
@Test
public void test(){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = new Teacher(2,"李长歌");
mapper.addTeacher(teacher);
//不提交事务,不更新数据
sqlSession.commit();
sqlSession.close();
}
获取更新时记录的key(slelectKey)
https://blog.csdn.net/kongkongyanan/article/details/86096657
<insert id="insertStudent">
<!--通过LAST_INSERT_ID() 获得刚插入的自动增长的id的值。
order属性设置成BEFORE,表示在插入前就获取对应的键
-->
<selectKey keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID() AS ID
</selectKey>
insert into student values(#{id},#{name},#{tid})
</insert>
Student student = new Student();
student.setName("张三");
student.setTid(1);
mapper.insertStudent(student);
System.out.println(student.getId());
模糊查询
select * from teacher where name like concat('%',#{name},'%');
Teacher teacher = mapper.getTeacherByName("长歌");
配置优化
xml规定了所有标签的顺序,必须完全符合规定
properties,settings,
typeAliases,typeHandlers,
objectFactory,objectWrapperFactory,
reflectorFactory,plugins,
environments,databaseIdProvider,
mappers
多环境配置
<environments default="test">
<environment id="development">
<!--environment必须配置事务管理器和dataSource
mybatis默认事务管理器是jdbc,但是还有一个,只要记得不只一个就行了
-->
<transactionManager type="JDBC"/>
<!-- 数据库连接池:用完不关,可以回收让别人再用
连接池状态是pooled(连接),还有unPooled和jedi(现在不用了)-->
<dataSource type="POOLED">
</dataSource>
</environment>
<environment id="test">
<transactionManager type="jdbc"/>
<dataSource type="pooled">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
属性优化properties 替换需要动态配置的属性值
<properties resource="jdbc.properties">
<property name="username" value="root"/>
</properties>
<dataSource type="pooled">
<!--先读取properties里的,再去读取${username}对应的配置文件中的属性,然后后来的覆盖前面的-->
<property name="username" value="${username}"/>
</dataSource>
类型别名: 降低冗余的全限定类名书写
<typeAliases>
<typeAlias type="com.changGe.li.pojo.Teacher" alias="teacher"></typeAlias>
<!--自动为包下所有类,创建首字母小写的别名-->
<package name="com.changGe.li.pojo"/>
</typeAliases>
<select id="getTeacherByName" resultType="teacher">
注解设置别名
@Alias("teacher")
public class Teacher {}
但是需要在mybatis-config.xml中,配置在哪个包下扫描注解
<typeAliases>
<package name="com.changGe.li.pojo"/>
</typeAliases>
系统默认别名
基本类型加_,包装类是对应的基本类型,集合首字母小写
int 别名是 _int,Integer = int, Map = map
<select id="getTeacherByName" resultType="teacher" parameterType="_int">
settings系统配置:改变 MyBatis 的运行时行为
可以设置如日志,缓存和懒加载
slf4j依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha4</version>
</dependency>
<settings>
<!--MyBatis默认的日志-->
<setting name="logImpl" value="SLF4J"/>
<!--缓存-->
<setting name="cacheEnabled" value="true"/>
<!--懒加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
MapperGesitry注册,映射器: 直接告诉 MyBatis 到哪里去找映射文件
当用实现类或包注册时,要求:实现类和配置文件与接口同名,且同包!
<mappers>
<!--1.当映射文件与接口同名,且同包时,写上接口名,会自动找到对应的映射文件-->
<!--2.或者写上原接口的完全限定名,mybatisX插件会自动根据方法结构来解析出sql语句-->
<mapper class="com.changGe.li.mapper.TeacherMapper"/>
<!--将包内的映射器接口实现全部注册为映射器-->
<package name="com/changGe/li/mapper"/>
</mappers>
如果想分离,要在resource包下创建和接口相同结构的包.
因为mybatis的resource是通过classpath来找文件的
生命周期和作用域
错误的使用可能会出现严重的并发问题
- 程序开始,配置文件被sqlsessionfactoryBuilder构建,
- 得到sqlsessionfatory工厂模式,
- 由此创建sqlsession对象,
- 产生对应接口的SQLMapper对象,
- 执行数据库操作,最后结束.
从 XML 中构建 SqlSessionFactory
- SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
- 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。
从 SqlSessionFactory 中获取 SqlSession
-
我们可以从SqlSessionFactory中获得 SqlSession 的实例。
-
SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。
通过 SqlSession 实例来直接执行已映射的 SQL 语句
-
sqlsession执行rselectOne()或者selectList()等方法,直接执行sql操作,最后结束
-
这种方式对使用旧版本 MyBatis 的用户来说,比较熟悉。
更简洁的方式
-
使用指定了语句的参数和返回值相匹配的接口(比如 BlogMapper.class)
-
现在代码更清晰,更加类型安全,不用担心可能出错的字符串字面值,以及强制类型转换。
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
作用域
结果集映射:解决数据库字段与对象属性不对应的问题
作用就是:把数据库中返回的字段的值,映射到对象的属性上
对象中有id和age两个属性
@Data
@AllArgsConstructor
@NoArgsConstructor
@Alias("teacher")
public class Teacher {
private int id;
private String ame;
}
配置结果集映射
<!--这里的id对应着select标签中的resultMap
返回值类型是Teacher类型
-->
<resultMap id="tea" type="teacher">
<id column="id" property="id"/>
<!--把数据库中返回的字段name中的值,映射到对象的ame属性上-->
<result column="name" property="ame"/>
</resultMap>
<select id="getTeacherByName" resultMap="tea" parameterType="teacher">
select * from teacher where id = #{id} and name = #{ame};
</select>
执行查询
Teacher teacher1 = new Teacher(2, "李长歌");
Teacher teacher = mapper.getTeacherByName(teacher1);
写resultMap时,resultType就可以不用写了
日志工厂
Mybatis 通过使用内置的日志工厂提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
-
MyBatis 内置日志工厂:会基于运行时检测信息,选择日志委托实现。
-
它会(按上面罗列的顺序)使用第一个查找到的实现。
-
如果你的环境中并不存在 Log4J,你却试图调用了相应的方法,MyBatis 就会忽略这一切换请求 .
-
当没有找到这些实现时,将会禁用日志功能。
STDOUT_LOGGING(标准输出记录)
xml声明
<settings>
<!--可选的值有SLF4J、LOG4J、LOG4J2、JDK_LOGGING、
COMMONS_LOGGING、STDOUT_LOGGING(标准输出记录)、NO_LOGGING-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
执行结果
//读者入口
Reader entry: <?xml version="1.0" encoding="UTF-8" ?>
//检查类 com.changGe.li.mapper.StudentMapper 是否符合条件 [可分配给对象]
Checking to see if class com.changGe.li.mapper.StudentMapper matches criteria [is assignable to Object]
Checking to see if class com.changGe.li.mapper.TeacherMapper matches criteria [is assignable to Object]
//打开 JDBC 连接
Opening JDBC Connection
//创建连接 1312381159
Created connection 1312381159.
//在 JDBC 连接 [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7] 上将 autocommit(自动提交) 设置为 false
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7]
//准备中
==> Preparing: select * from teacher where id = ? and name = ?;
//参数
==> Parameters: 2(Integer), 李长歌(String)
//列
<== Columns: id, name
//行
<== Row: 2, 李长歌
//总计
<== Total: 1
//返回结果
Teacher(id=2, name=李长歌)
//在 JDBC 连接 [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7] 上将自动提交重置为 true
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7]
//关闭 JDBC 连接 [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e3958e7]
//将连接 1312381159 返回到池
Returned connection 1312381159 to pool.
Log4j :java日志
使用Log4j框架的作用通俗的解释:
- 能够控制日志信息想往哪里打就往哪里打,比如:控制台、文件、邮箱、数据库等等。
- 能够控制日志信息想怎么打就怎么打,比如:我想要打印时间、程序的名称、程序的方法名、程序的行号、线程的名称等等。
- 能够控制日志信息想打什么打什么,不想打的就不打,日志信息是分级别的,有时候我只想看错误的信息或者警告的信息,有时候我想看到所有的信息我想调试程序等等。
依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
log4j.properties
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=【%c】-%m%n
#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=【%p】【%d{yy-MM-dd}】【%c】%m%n
#日志输出级别:输出DEBUG级别及以上的日志信息
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
简单使用
public class MyBatisTest {
//在当前类运行时,输出日志
private static Logger logger = Logger.getLogger(MyBatisTest.class);
@Test
public void test(){
//在debug时,输出当前为debug模式
logger.debug("当前为debug模式");
SqlSession sqlSession = MyBatisUtil.getSqlSession();
//输出一条普通信息:打印sqlSession
logger.info(sqlSession);
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher1 = new Teacher(2, "李长歌");
Teacher teacher = mapper.getTeacherByName(teacher1);
System.out.println(teacher);
sqlSession.close();
try {}catch (Exception e){}
finally {
logger.error("报错了");
}
}
}
日志文件的存放地址
部分日志信息
分页
RowBounds
<select id="getStudent" resultType="student">
select * from student;
</select>
@Test
public void test(){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
//分页,从3开始显示,显示4个
RowBounds rowBounds = new RowBounds(2, 4);
//方法路径,参数和分页
List<Student> students = sqlSession.selectList("com.changGe.li.mapper.StudentMapper.getStudent", null, rowBounds);
for (com.changGe.li.pojo.Student student : students) {
System.out.println(student);
}
sqlSession.close();
}
MyBatis的分页插件:PageHelper: MyBatis 分页插件 PageHelper
注解开发
根本目的为了解耦
@Select("select * from student;")
List<Student> getStudent();
- 运行的本质是:sqlSession(User.class)获取User的类对象,
- 然后获取其中所有的数据,读取特定数据上的注解,
- 得到其中的value(就是我们写的sql语句),
- 自动帮我们配置环境,运行.
Mapper中有一个sqlSession,里面保存了所有的配置信息
增删改
TeacherMapper
@Insert("insert into teacher values(#{id},#{name});")
/**
* 将传参id的值交给@Param中的id,然后赋值给sql语句中的#{id}
*
* @Param中的参数名必须与#{}一致,mybatis就是从这里取值的
*/
int insertTeacher(@Param("id")int id,@Param("name")String name);
//设置开启自动提交
return sqlSessionFactory.openSession(true);
注解里的需要的参数,会自动去方法传参中找.
比如传参是对象,就会去自动匹配对象的字段。
@Insert("insert into teacher values(#{id},#{name});")
/**
* 注解里的需要的参数,会自动去方法传参中找.
*/
int insertTeacher(Teacher teacher);
mapper.insertTeacher(new Teacher(4,"太平公主"));
运行分析
MyBatis运行流程(源码):https://zhuanlan.zhihu.com/p/67738448
简略易读版本: MyBatis的执行流程详解 - 知乎 (zhihu.com)
-
Resource类读取配置文件,创建SqlSessionFactroyBuilder对象
//读取配置文件的流 InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml"); //sql会话工厂构建类 构建 得到sql会话工厂 sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
-
这个对象底层用流,创建了一个XMLConfig对象.
public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); }
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //通过流读取properties文件,创建XMLConfigBUiler对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //XMLConfig对象读取xml配置文件 //解析文件中数据,返回SqlSessionFactory对象 return build(parser.parse()); } catch (Exception e) {} finally { ErrorContext.instance().reset(); } }
-
XMLConfigBUiler对象读取xml配置文件,创建Configuration对象,这个对象中包含了所有的基本配置信息
sqlSession中包含了configuration,里面有所有的配置信息,如environment(环境)
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
-
SqlSessionFactroy工厂对象,构建SqlSession对象(openSession)时,底层创建Transactional事务管理器–监听事务.同时创建Execetor执行器
sqlSession中包含executor和transaction对象
return sqlSessionFactory.openSession();
SqlSessionFactory的实现类:DefaultSqlSessionFactory
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
//新建事务管理器
Transaction tx = null;
try {
//获取环境
final Environment environment = configuration.getEnvironment();
//从环境中获取:事务管理器工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//事务管理器赋值:所有的环境数据,等级和是否自动提交
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建执行器,专门用来执行sql操作,和缓存等
final Executor executor = configuration.newExecutor(tx, execType);
//把事务管理器和执行器都返回
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
//关闭事务管理器
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
}
}
lombok
它是一个插件,还需要maven依赖(也可以直接引用依赖)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
@Data @AllArgsConstractor @NoArgsConstractor
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private int id;
private String name;
private int tid;
}
有一种说法是lombok改变了java的源码,让java语言出现了"文明断层"的情况