【Mybatis】源码分析-高级应用

1、Mybatis配置文件深入理解

1.2、动态SQL语句

        Mybatis 的映射⽂件中,前⾯我们的 SQL 都是⽐较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化的,此时在前⾯的学习中我们的 SQL 就不能满⾜要求了。

1.2.1、条件判断

        我们根据实体类的不同取值,使⽤不同的 SQL语句来进⾏查询。⽐如在 id如果不为空时可以根据id查询,如果username 不同空时还要加⼊⽤户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。

<select id="findByCondition" parameterType="user" resultType="user">select * from User<where><if test="id!=0">and id=#{id}</if><if test="username!=null">and username=#{username}</if></where>
</select>

        当查询条件id和username都存在时,控制台打印的sql语句如下:

//获得MyBatis框架⽣成的UserMapper接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User condition = new User();
condition.setId(1);
condition.setUsername("lucy");
User user = userMapper.findByCondition(condition);

        当查询条件只有id存在时,控制台打印的sql语句如下:

//获得MyBatis框架⽣成的UserMapper接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User condition = new User();
condition.setId(1);
User user = userMapper.findByCondition(condition);

1.2.2、循环执行

        循环执⾏sql的拼接操作,例如:SELECT * FROM USER WHERE id IN (1,2,5)。

<select id="findByIds" parameterType="list" resultType="user">select * from User<where><foreach collection="list" open="id in(" close=")" item="id"
separator=",">#{id}</foreach></where>
</select>

测试代码⽚段如下:

//获得MyBatis框架⽣成的UserMapper接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int[] ids = new int[]{2,5};
List<User> userList = userMapper.findByIds(ids);
System.out.println(userList);

foreach标签的属性含义如下:

标签⽤于遍历集合,它的属性:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}
  • open:代表语句的开始部分
  • close:代表结束部分
  • item:代表遍历集合的每个元素,⽣成的变量名
  • sperator:代表分隔符

1.2.3、SQL⽚段抽取

        Sql 中可将重复的 sql 提取出来,使⽤时⽤ include 引⽤即可,最终达到 sql 重⽤的⽬的。

<!--抽取sql⽚段简化编写-->
<sql id="selectUser" select * from User</sql><select id="findById" parameterType="int" resultType="user"><include refid="selectUser"></include> where id=#{id}
</select><select id="findByIds" parameterType="list" resultType="user"><include refid="selectUser"></include><where><foreach collection="array" open="id in(" close=")" item="id"
separator=",">#{id}</foreach></where>
</select>

2、Mybatis 复杂映射开发

2.1、一对一查询

2.1.1、查询模型

        ⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户;⼀对⼀查询的需求:查询⼀个订单,与此同时查询出该订单所属的⽤户。

2.1.2、查询语句

select * from orders o,user u where o.uid=u.id;

2.1.3、实体创建

public class Order {private int id;private Date ordertime;private double total;//代表当前订单从属于哪⼀个客户private User user;
}public class User {private int id;private String username;private String password;private Date birthday;
}

2.1.4、创建Mapper接口

public interface OrderMapper {List<Order> findAll();
}

2.1.5、创建Mapper.xml

<mapper namespace="com.blnp.net.mapper.OrderMapper"><resultMap id="orderMap" type="com.blnp.net.domain.Order"><result column="uid" property="user.id"></result><result column="username" property="user.username"></result><result column="password" property="user.password"></result><result column="birthday" property="user.birthday"></result></resultMap><select id="findAll" resultMap="orderMap">select * from orders o,user u where o.uid=u.id</select>
</mapper>

其中还可以配置如下:

<resultMap id="orderMap" type="com.blnp.net.domain.Order"><result property="id" column="id"></result><result property="ordertime" column="ordertime"></result><result property="total" column="total"></result><association property="user" javaType="com.blnp.net.domain.User"><result column="uid" property="id"></result><result column="username" property="username"></result><result column="password" property="password"></result><result column="birthday" property="birthday"></result></association>
</resultMap>

2.1.6、测试查询结果

OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Order> all = mapper.findAll();
for(Order order : all){System.out.println(order);
}

2.2、一对多查询

2.2.1、查询模型

        ⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户;⼀对多查询的需求:查询⼀个⽤户,与此同时查询出该⽤户具有的订单。

2.2.2、查询语句

select *,o.id oid from user u left join orders o on u.id=o.uid;

2.2.3、修改实体

public class Order {private int id;private Date ordertime;private double total;//代表当前订单从属于哪⼀个客户private User user;
}public class User {private int id;private String username;private String password;private Date birthday;//代表当前⽤户具备哪些订单private List<Order> orderList;
}

2.2.4、创建Mapper接口

public interface UserMapper {List<User> findAll();
}

2.2.5、配置UserMapper.xml

<mapper namespace="com.blnp.net.mapper.UserMapper"><resultMap id="userMap" type="com.blnp.net.domain.User"><result column="id" property="id"></result><result column="username" property="username"></result><result column="password" property="password"></result><result column="birthday" property="birthday"></result><collection property="orderList" ofType="com.blnp.net.domain.Order"><result column="oid" property="id"></result><result column="ordertime" property="ordertime"></result><result column="total" property="total"></result></collection></resultMap><select id="findAll" resultMap="userMap">select *,o.id oid from user u left join orders o on u.id=o.uid</select>
</mapper>

2.2.6、查询结果测试

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAll();
for(User user : all){System.out.println(user.getUsername());List<Order> orderList = user.getOrderList();for(Order order : orderList){System.out.println(order);}System.out.println("----------------------------------");
}

2.3、多对多查询

2.3.1、查询模型

        ⽤户表和⻆⾊表的关系为,⼀个⽤户有多个⻆⾊,⼀个⻆⾊被多个⽤户使⽤;多对多查询的需求:查询⽤户同时查询出该⽤户的所有⻆⾊。

2.3.2、查询语句

select u.,r.,r.id rid from user u left join user_role ur on u.id=ur.user_id inner join role r on ur.role_id=r.id;

2.3.3、实体创建

public class User {private int id;private String username;private String password;private Date birthday;//代表当前⽤户具备哪些订单private List<Order> orderList;//代表当前⽤户具备哪些⻆⾊private List<Role> roleList;
}public class Role {private int id;private String rolename;
}

2.3.4、创建Mapper接口

List<User> findAllUserAndRole();

2.3.5、配置Mapper.xml

<resultMap id="userRoleMap" type="com.blnp.net.domain.User"><result column="id" property="id"></result><result column="username" property="username"></result><result column="password" property="password"></result><result column="birthday" property="birthday"></result><collection property="roleList" ofType="com.blnp.net.domain.Role"><result column="rid" property="id"></result><result column="rolename" property="rolename"></result></collection>
</resultMap><select id="findAllUserAndRole" resultMap="userRoleMap">select u.*,r.*,r.id rid from user u left join user_role ur onu.id=ur.user_idinner join role r on ur.role_id=r.id
</select>

2.3.6、查询结果测试

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){System.out.println(user.getUsername());List<Role> roleList = user.getRoleList();for(Role role : roleList){System.out.println(role);}System.out.println("----------------------------------");
}

3、Mybatis注解开发

3.1、常用注解

  1. @Insert:实现新增
  2. @Update:实现更新
  3. @Delete:实现删除
  4. @Select:实现查询
  5. @Result:实现结果集封装
  6. @Results:可以与@Result ⼀起使⽤,封装多个结果集
  7. @One:实现⼀对⼀结果集封装
  8. @Many:实现⼀对多结果集封装

3.2、MyBatis的增删改查

private UserMapper userMapper;@Before
public void before() throws IOException
{InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession(true);userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void testAdd()
{User user = new User();user.setUsername("测试数据");user.setPassword("123");user.setBirthday(new Date());userMapper.add(user);
}
@Test
public void testUpdate() throws IOException
{User user = new User();user.setId(16);user.setUsername("测试数据修改");user.setPassword("abc");user.setBirthday(new Date());userMapper.update(user);
}
@Test
public void testDelete() throws IOException
{userMapper.delete(16);
}
@Test
public void testFindById() throws IOException
{User user = userMapper.findById(1);System.out.println(user);
}
@Test
public void testFindAll() throws IOException
{List < User > all = userMapper.findAll();for(User user: all){System.out.println(user);}
}

        修改MyBatis的核⼼配置⽂件,我们使⽤了注解替代的映射⽂件,所以我们只需要加载使⽤了注解的Mapper接⼝即可。

<mappers><!--扫描使⽤注解的类--><mapper class="com.blnp.net.mapper.UserMapper"></mapper>
</mappers>

或者指定扫描包含映射关系的接⼝所在的包也可以:

<mappers><!--扫描使⽤注解的类所在的包--><package name="com.blnp.net.mapper"></package>
</mappers>

3.3、复杂映射开发

        实现复杂关系映射之前我们可以在映射⽂件中通过配置来实现,使⽤注解开发后,我们可以使⽤@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置。

注解说明
@Results代替的是标签<resultMap> 该注解中可以使用单个@Result 注解,也可以使用@Result 集合。使用格式: @Results ({@Result(),@Result()}) 或者 @Results(@Result())
@Result代替了<id> 标签 和 <result> 标签: @Result 注解属性介绍: column:数据库的列名 property:需要装配的属性名 one:需要使用的@One注解(@Result(one=@One)()) many:需要使用的@Many注解(@Result (many=@many)() )
@One代替了<assocation>标签,是多查询的关键,在注解中用来指定子查询返回单一对象。 @One 注解属性说明: select:指定用来多表查询的 sqlmapper 使用格式:@Result(column="",property="",one=@One(select=""))
@Many代替了<collection>标签,是多表查询的关键,在注解中用来指定子查询返回对象的集合。 使用格式:@Result(property="",column="",many=@Many(select=""))

3.4、一对一查询(注解)

3.4.1、查询模型

3.4.2、查询语句

select * from orders;select * from user where id=查询出订单的uid;

3.4.3、创建实体

public class Order {private int id;private Date ordertime;private double total;//代表当前订单从属于哪⼀个客户private User user;
}public class User {private int id;private String username;private String password;private Date birthday;
}

3.4.4、创建mapper接口

public interface OrderMapper {List<Order> findAll();
}

3.4.5、使⽤注解配置Mapper

public interface OrderMapper {@Select("select * from orders")@Results({@Result(id=true,property = "id",column = "id"),@Result(property = "ordertime",column = "ordertime"),@Result(property = "total",column = "total"),@Result(property = "user",column = "uid",javaType = User.class,one = @One(select ="com.blnp.net.mapper.UserMapper.findById"))})List<Order> findAll();
}
public interface UserMapper {@Select("select * from user where id=#{id}")User findById(int id);
}

3.4.6、查询结果

@Test
public void testSelectOrderAndUser() {List<Order> all = orderMapper.findAll();for(Order order : all){System.out.println(order);}
}

3.5、一对多查询(注解)

3.5.1、查询模型

3.5.2、查询语句

select * from user;select * from orders where uid=查询出⽤户的id;

3.5.3、创建实体

public class Order {private int id;private Date ordertime;private double total;//代表当前订单从属于哪⼀个客户private User user;
}public class User {private int id;private String username;private String password;private Date birthday;//代表当前⽤户具备哪些订单private List<Order> orderList;
}

3.5.4、创建Mapper接口

List<User> findAllUserAndOrder();

3.5.5、使⽤注解配置Mapper

public interface UserMapper {@Select("select * from user")@Results({@Result(id = true,property = "id",column = "id"),@Result(property = "username",column = "username"),@Result(property = "password",column = "password"),@Result(property = "birthday",column = "birthday"),@Result(property = "orderList",column = "id",javaType = List.class,many = @Many(select =
"com.blnp.net.mapper.OrderMapper.findByUid"))
})List<User> findAllUserAndOrder();
}public interface OrderMapper {@Select("select * from orders where uid=#{uid}")List<Order> findByUid(int uid);
}

3.5.6、查询结果

List<User> all = userMapper.findAllUserAndOrder();
for(User user : all){System.out.println(user.getUsername());List<Order> orderList = user.getOrderList();for(Order order : orderList){System.out.println(order);}System.out.println("-----------------------------");
}

3.6、多对多查询(注解)

3.6.1、查询模型

3.6.2、查询语句

select * from user;select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=⽤户的id;

3.6.3、创建实体

public class User {private int id;private String username;private String password;private Date birthday;//代表当前⽤户具备哪些订单private List<Order> orderList;//代表当前⽤户具备哪些⻆⾊private List<Role> roleList;
}public class Role {private int id;private String rolename;
}

3.6.4、添加UserMapper接⼝⽅法

List<User> findAllUserAndRole();

3.6.5、使⽤注解配置Mapper

public interface UserMapper {@Select("select * from user")@Results({@Result(id = true,property = "id",column = "id"),@Result(property = "username",column = "username"),@Result(property = "password",column = "password"),@Result(property = "birthday",column = "birthday"),@Result(property = "roleList",column = "id",javaType = List.class,many = @Many(select =
"com.blmnp.net.mapper.RoleMapper.findByUid"))
})List<User> findAllUserAndRole();
}public interface RoleMapper {@Select("select * from role r,user_role ur where r.id=ur.role_id andur.user_id=#{uid}")List<Role> findByUid(int uid);
}

3.6.6、查询结果

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){System.out.println(user.getUsername());List<Role> roleList = user.getRoleList();for(Role role : roleList){System.out.println(role);}System.out.println("----------------------------------");
}

4、Mybatis缓存

4.1、一级缓存

4.1.1、场景一

        在⼀个sqlSession中,对User表根据id进⾏两次查询,查看他们发出sql语句的情况。

@Test
public void test1(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);//第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中User u1 = userMapper.selectUserByUserId(1);System.out.println(u1);//第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果//如果有,则直接从缓存中取出来,不和数据库进⾏交互User u2 = userMapper.selectUserByUserId(1);System.out.println(u2);sqlSession.close();
}

查看控制台打印情况:

4.1.2、场景二

        同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。

@Test
public void test2(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中User u1 = userMapper.selectUserByUserId( 1 );System.out.println(u1);//第⼆步进⾏了⼀次更新操作,sqlSession.commit()u1.setSex("⼥");userMapper.updateUserByUserId(u1);sqlSession.commit();//第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息//则此次查询也会发出sql语句User u2 = userMapper.selectUserByUserId(1);System.out.println(u2);sqlSession.close();
}

查看控制台打印情况:

4.1.3、小结

        1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,则从数据库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。

        2、 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。

        3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直接从缓存中获取⽤户信息

4.1.4、原理分析

        上⾯我们⼀直提到⼀级缓存,那么提到⼀级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者⽅法。

        调研了⼀圈,发现上述所有⽅法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入⼿吧,分析源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别⼜是谁,对如上关系了解了,你才会对这个类有更深的认识,分析了⼀圈,你可能会得到如下这个流程图。

        再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那 么这个cache是什么东⻄呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是⼀个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是本地存放的⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤,那么这个cache是何时创建的呢?

        你觉得最有可能创建缓存的地⽅是哪⾥呢?我觉得是Executor,为什么这么认为?因为Executor是执⾏器,⽤来执⾏SQL请求,⽽且清除缓存的⽅法也在Executor中执⾏,所以很可能缓存的创建也很有可能在Executor中,看了⼀圈发现Executor中有⼀个createCacheKey⽅法,这个⽅法很像是创建缓存的⽅法啊,跟进去看看,你发现createCacheKey⽅法是由BaseExecutor执⾏的,代码如下:

CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后⾯是update 了 sql中带的参数
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}

        创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个update⽅法最终由updateList的list来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能 理解这五个值都是什么了:

        这⾥需要注意⼀下最后⼀个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis-config.xml中的标签,⻅如下。

<environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/></dataSource></environment>
</environments>

        那么我们回归正题,那么创建完缓存之后该⽤在何处呢?总不会凭空创建⼀个缓存不使⽤吧?绝对不会的,经过我们对⼀级缓存的探究之后,我们发现⼀级缓存更多是⽤于查询操作,毕竟⼀级缓存也叫做查询缓存吧,为什么叫查询缓存我们⼀会⼉说。我们先来看⼀下这个缓存到底⽤在哪了,我们跟踪到query⽅法如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);//创建缓存CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}@SuppressWarnings("unchecked")
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {...list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {//这个主要是处理存储过程⽤的。handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key,boundSql);}...
}// queryFromDatabase ⽅法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql
boundSql) throws SQLException {List<E> list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}

        如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进⾏写⼊。 localcache对象的put⽅法最终交给Map进⾏存放.

private Map<Object, Object> cache = new HashMap<Object, Object>();@Overridepublic void putObject(Object key, Object value) { cache.put(key, value);
}

4.2、二级缓存

        ⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去缓存中取。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也就是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中。

4.2.1、开启二级缓存

        和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启。⾸先在全局配置⽂件sqlMapConfig.xml⽂件中加⼊如下代码:

<!--开启⼆级缓存-->
<settings><setting name="cacheEnabled" value="true"/>
</settings>

其次在UserMapper.xml⽂件中开启缓存:

<!--开启⼆级缓存-->
<cache></cache>

        我们可以看到mapper.xml⽂件中就这么⼀个空标签,其实这⾥可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使⽤mybatis默认的缓存,也可以去实现Cache接口来⾃定义缓存。

public class PerpetualCache implements Cache {private final String id;private Map<Object, Object> cache = new HashMap();public PerpetualCache(String id) { this.id = id;
}

我们可以看到⼆级缓存底层还是HashMap结构:

public class User implements Serializable(//⽤户IDprivate int id;//⽤户姓名private String username;//⽤户性别private String sex;
}

        开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接⼝.

4.2.2、测试验证

1、测试⼆级缓存和sqlSession⽆关
@Test
public void testTwoCache(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession1 = sessionFactory.openSession();SqlSession sqlSession2 = sessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中User u1 = userMapper1.selectUserByUserId(1);System.out.println(u1);sqlSession1.close(); //第⼀次查询完后关闭 sqlSession//第⼆次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句User u2 = userMapper2.selectUserByUserId(1);System.out.println(u2);sqlSession2.close();
}

        可以看出上⾯两个不同的sqlSession,第⼀个关闭了,第⼆次查询依然不发出sql查询语句。

2、测试执⾏commit()操作,⼆级缓存数据清空
@Test
public void testTwoCache(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession1 = sessionFactory.openSession();SqlSession sqlSession2 = sessionFactory.openSession();SqlSession sqlSession3 = sessionFactory.openSession();String statement = "com.blnp.net.pojo.UserMapper.selectUserByUserld" ;UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class );//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中User u1 = userMapperl.selectUserByUserId( 1 );System.out.println(u1);sqlSessionl .close(); //第⼀次查询完后关闭sqlSession//执⾏更新操作,commit()u1.setUsername( "aaa" );userMapper3.updateUserByUserId(u1);sqlSession3.commit();//第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语User u2 = userMapper2.selectUserByUserId( 1 );System.out.println(u2);sqlSession2.close();
}

4.2.3、useCache和flushCache

        mybatis中还可以配置userCache和flushCache等配置项,userCache是⽤来设置是否禁⽤⼆级缓存的,在statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存,即每次查询都会发出 sql去查询,默认情况是true,即该sql使⽤⼆级缓存。

<select id="selectUserByUserId" useCache="false"resultType="com.blnp.net.pojo.User" parameterType="int">select * from user where id=#{id}
</select>

        这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁⽤⼆级缓存,直接从数据库中获取。

        在mapper的同⼀个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如果不执⾏刷新缓存会出现脏读。

        设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使⽤缓存时如果⼿动修改数据库表中的查询数据会出现脏读。

<select id="selectUserByUserId" flushCache="true" useCache="false"resultType="com.blnp.net.pojo.User" parameterType="int">select * from user where id=#{id}
</select>

        ⼀般下执⾏完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不⽤设置,默认即可.

4.3、⼆级缓存整合redis

        上⾯我们介绍了 mybatis⾃带的⼆级缓存,但是这个缓存是单服务器⼯作,⽆法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,⽤户访问的时候访问了1服务器,查询后的缓存就会放在1服务器上,假设现在有个⽤户访问的是2服务器,那么他在2服务器上就⽆法获取刚刚那个缓存,如下图所示:

        为了解决这个问题,就得找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数据都往它那⾥存,取缓存数据也从它那⾥取,如下图所示:

        如上图所示,在⼏个不同的服务器之间,我们使⽤第三⽅缓存框架,将缓存都放在这个第三⽅框架中,然后⽆论有多少台服务器,我们都能从缓存中获取数据。

        刚刚提到过,mybatis提供了⼀个eache接⼝,如果要实现⾃⼰的缓存逻辑,实现cache接⼝开发即可。mybatis本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以我们要⾃⼰来实现。redis分布式缓存就可以,mybatis提供了⼀个针对cache接⼝的redis实现类,该类存在mybatis-redis包中。

4.3.1、pom坐标

<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>

4.3.2、配置文件

Mapper.xml

<?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.blnp.net.mapper.IUserMapper">
<cache type="org.mybatis.caches.redis.RedisCache" /><select id="findAll" resultType="com.blnp.net.pojo.User" useCache="true">select * from user
</select>

4.3.3、Redis配置

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

4.3.4、验证测试

@Test
public void SecondLevelCache(){SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();SqlSession sqlSession3 = sqlSessionFactory.openSession();IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);IUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);User user1 = mapper1.findUserById(1);sqlSession1.close(); //清空⼀级缓存User user = new User();user.setId(1);user.setUsername("lisi");mapper3.updateUser(user);sqlSession3.commit();User user2 = mapper2.findUserById(1);System.out.println(user1==user2);
}

4.3.5、源码分析

        RedisCache和⼤家普遍实现Mybatis的缓存⽅案⼤同⼩异,⽆⾮是实现Cache接⼝,并使⽤jedis操作缓存;不过该项⽬在设计细节上有⼀些区别;

public final class RedisCache implements Cache {public RedisCache(final String id) {if (id == null) {throw new IllegalArgumentException("Cache instances require anID");}this.id = id;RedisConfig redisConfig =RedisConfigurationBuilder.getInstance().parseConfiguration();pool = new JedisPool(redisConfig, redisConfig.getHost(),redisConfig.getPort(),redisConfig.getConnectionTimeout(),redisConfig.getSoTimeout(), redisConfig.getPassword(),redisConfig.getDatabase(), redisConfig.getClientName());}
}

        RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的⽅式很简单,就是调⽤RedisCache的带有String参数的构造⽅法,即RedisCache(String id);⽽在RedisCache的构造⽅法中,调⽤了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使⽤ RedisConfig 来创建JedisPool。RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看⼀下RedisConfig的属性:

public class RedisConfig extends JedisPoolConfig {private String host = Protocol.DEFAULT_HOST;private int port = Protocol.DEFAULT_PORT;private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;private int soTimeout = Protocol.DEFAULT_TIMEOUT;private String password;private int database = Protocol.DEFAULT_DATABASE;private String clientName;
}

        RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要⽅法:

public RedisConfig parseConfiguration(ClassLoader classLoader) {Properties config = new Properties();InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);if (input != null) {try {config.load(input);} catch (IOException e) {throw new RuntimeException("An error occurred while reading classpath property '"+ redisPropertiesFilename+ "', see nested exceptions", e);} finally {try {input.close();} catch (IOException e) {// close quietly}}}RedisConfig jedisConfig = new RedisConfig();setConfigProperties(config, jedisConfig);return jedisConfig;
}

        核⼼的⽅法就是parseConfiguration⽅法,该⽅法从classpath中读取⼀个redis.properties⽂件,并将该配置⽂件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使⽤RedisConfig类创建完成redisPool;在RedisCache中实现了⼀个简单的模板⽅法,⽤来操作Redis:

private Object execute(RedisCallback callback) {Jedis jedis = pool.getResource();try {return callback.doWithRedis(jedis);} finally {jedis.close();}
}

        模板接⼝为RedisCallback,这个接⼝中就只需要实现了⼀个doWithRedis⽅法⽽已:

public interface RedisCallback {Object doWithRedis(Jedis jedis);
}

        接下来看看Cache中最重要的两个⽅法:putObject和getObject,通过这两个⽅法来查看mybatis-redis储存数据的格式:

@Override
public void putObject(final Object key, final Object value)
{execute(new RedisCallback(){@Overridepublic Object doWithRedis(Jedis jedis){jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));return null;}});
}
@Override
public Object getObject(final Object key)
{return execute(new RedisCallback(){@Overridepublic Object doWithRedis(Jedis jedis){return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));}});
}

        可以很清楚的看到,mybatis-redis在存储数据的时候,是使⽤的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;

5、Mybatis插件

5.1、插件简介

        ⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯作。以MyBatis为例,我们可基于MyBatis插件机制实现分⻚、分表,监控等功能。由于插件和业务⽆关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能。

5.2、Mybatis插件介绍

        Mybatis作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插件扩展机制。Mybatis对持久层的操作就是借助于四⼤核⼼对象。MyBatis⽀持⽤插件对四⼤核⼼对象进⾏拦截,对mybatis来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四⼤对象都是代理对象。

MyBatis所允许拦截的⽅法如下:

  1. 执⾏器Executor (update、query、commit、rollback等⽅法);
  2. SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅法);
  3. 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
  4. 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);

5.3、插件原理

        在四⼤对象创建的时候:

  • 1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包装后的对象
  • 3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可以为四⼤对象创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain)
{ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, object, sql);parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}
public Object pluginAll(Object target)
{for(Interceptor interceptor: interceptors){target = interceptor.plugin(target);}return target;
}

        interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链中的拦截器依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四⼤对象。返回的target是被重重代理后的对象。如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:

@Intercepts(
{@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})
})
public class ExamplePlugin implements Interceptor
{//省略逻辑
}

        除此之外,我们还需将插件配置到sqlMapConfig.xml中。

<plugins><plugin interceptor="com.blnp.net.plugin.ExamplePlugin"></plugin>
</plugins>

        这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备⼯作做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例⽣成代理类。这样,插件逻辑即可在 Executor相关⽅法被调⽤前执⾏。以上就是MyBatis插件机制的基本原理。

5.4、自定义插件

5.4.1、自定义插件接口

Mybatis 插件接⼝-Interceptor

  • Intercept⽅法,插件的核⼼⽅法
  • plugin⽅法,⽣成target的代理对象
  • setProperties⽅法,传递插件所需参数

5.4.2、自定义插件

设计实现⼀个⾃定义插件

Intercepts(
{ //注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器@Signature(type = StatementHandler.class, //这是指拦截哪个接⼝method = "prepare", //这个接⼝内的哪个⽅法名,不要拼错了args = {Connection.class,Integer.class}), // 这是拦截的⽅法的⼊参,按顺序写到这, 不要多也不要少, 如果⽅ 法重载, 可是要通过⽅ 法名和⼊ 参来确定唯⼀ 的
})
public class MyPlugin implements Interceptor
{private final Logger logger = LoggerFactory.getLogger(this.getClass());// //这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内@Overridepublic Object intercept(Invocation invocation) throws Throwable{//增强逻辑System.out.println("对⽅法进⾏了增强....");return invocation.proceed(); //执⾏原⽅法}/*** //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中* ^Description包装⽬标对象 为⽬标对象创建代理对象* @Param target为要拦截的对象* @Return代理对象*/@Overridepublic Object plugin(Object target){System.out.println("将要包装的⽬标对象:" + target);return Plugin.wrap(target, this);}/**获取配置⽂件的属性**///插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来@Overridepublic void setProperties(Properties properties){System.out.println("插件配置的初始化参数:" + properties);}
}

sqlMapConfig.xml

<plugins><plugin interceptor="com.blnp.net.plugin.MySqlPagingPlugin"><!--配置参数--><property name="name" value="Bob"/></plugin>
</plugins>

mapper接⼝

public interface UserMapper {List<User> selectUser();
}

mapper.xml

<mapper namespace="com.blnp.net.mapper.UserMapper"><select id="selectUser" resultType="com.blnp.net.pojo.User">SELECTid,usernameFROMuser</select>
</mapper>

测试方法

public class PluginTest
{@Testpublic void test() throws IOException{InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);List < User > byPaging = userMapper.selectUser();for(User user: byPaging){System.out.println(user);}}
}

5.5、源码分析

        Plugin实现了 InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。invoke⽅法会对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:

// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable
{try{/**获取被拦截⽅法列表,⽐如:* signatureMap.get(Executor.class), 可能返回 [query, update,commit]*/Set < Method > methods = signatureMap.get(method.getDeclaringClass());//检测⽅法列表是否包含被拦截的⽅法if(methods != null && methods.contains(method)){//执⾏插件逻辑return interceptor.intercept(new Invocation(target, method, args));//执⾏被拦截的⽅法return method.invoke(target, args);}catch (Exception e){}}

        invoke⽅法的代码⽐较少,逻辑不难理解。⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的@Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept中,该⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看⼀下该类的定义:

public class Invocation
{private final Object target;private final Method method;private final Object[] args;public Invocation(Object targetf Method method, Object[] args){this.target = target;this.method = method;//省略部分代码public Object proceed() throws InvocationTargetException,IllegalAccessException{ //调⽤被拦截的⽅法>> —>> ……

5.6、pageHelper分⻚插件

        MyBati s可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿PageHelper是将分⻚的复杂操作进⾏封装,使⽤简单的⽅式即可获得分⻚的相关数据。

开发步骤:

  1. 导⼊通⽤PageHelper的坐标
  2. 在mybatis核⼼配置⽂件中配置PageHelper插件
  3. 测试分⻚数据获取

5.6.1、导⼊通⽤PageHelper坐标

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>3.7.5</version>
</dependency><dependency><groupId>com.github.jsqlparser</groupId><artifactId>jsqlparser</artifactId><version>0.9.1</version>
</dependency>

5.6.2、配置PageHelper插件

<!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper"><!—指定⽅⾔ —><property name="dialect" value="mysql"/>
</plugin>

5.6.3、测试分页

@Test
public void testPageHelper()
{//设置分⻚参数PageHelper.startPage(1, 2);List < User > select = userMapper2.select(null);for(User user: select){System.out.println(user);}//其他分⻚的数据PageInfo < User > pageInfo = new PageInfo < User > (select);System.out.println("总条数:" + pageInfo.getTotal());System.out.println("总⻚数:" + pageInfo.getPages());System.out.println("当前⻚:" + pageInfo.getPageNum());System.out.println("每⻚显万⻓度:" + pageInfo.getPageSize());System.out.println("是否第⼀⻚:" + pageInfo.isIsFirstPage());System.out.println("是否最后⼀⻚:" + pageInfo.isIsLastPage());
}
}

5.7、通用Mapper

5.7.1、什么是通⽤Mapper

        通⽤Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发⼈员不需要编写SQL,不需要在DAO中增加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法。

5.7.2、导入坐标

<dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>3.1.2</version>
</dependency>

5.7.3、基本配置

<plugins><!--分⻚插件:如果有分⻚插件,要排在通⽤mapper之前--><plugin interceptor="com.github.pagehelper.PageHelper"><property name="dialect" value="mysql"/></plugin><plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor"><!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 --><property name="mappers" value="tk.mybatis.mapper.common.Mapper"/></plugin>
</plugins>

5.7.4、实体主键配置

@Table(name = "t_user")
public class User
{@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;private String username;
}

5.7.5、定义通用Mapper

import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper < User >
{}

5.7.6、验证测试

public class UserTest
{@Testpublic void test1() throws IOException{Inputstream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory build = newSqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = build.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);User user = new User();user.setId(4);//(1)mapper基础接⼝//select 接⼝User user1 = userMapper.selectOne(user); //根据实体中的属性进⾏查询,只能有—个返回值List < User > users = userMapper.select(null); //查询全部结果userMapper.selectByPrimaryKey(1); //根据主键字段进⾏查询,⽅法参数必须包含完整的主键属性, 查询条件使⽤ 等号userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使⽤等号// insert 接⼝int insert = userMapper.insert(user); //保存⼀个实体,null值也会保存,不会使⽤数据库默认值int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存,会使⽤ 数据库默认值// update 接⼝int i1 = userMapper.updateByPrimaryKey(user); //根据主键更新实体全部字段,null值会被更新// delete 接⼝int delete = userMapper.delete(user); //根据实体属性作为条件进⾏删除,查询条件 使⽤ 等号userMapper.deleteByPrimaryKey(1); //根据主键字段进⾏删除,⽅法参数必须包含完整的主键属性//(2)example⽅法Example example = new Example(User.class);example.createCriteria().andEqualTo("id", 1);example.createCriteria().andLike("val", "1");//⾃定义查询List < User > users1 = userMapper.selectByExample(example);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/23162.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

技巧:合并ZIP分卷压缩包

如果ZIP压缩文件文件体积过大&#xff0c;大家可能会选择“分卷压缩”来压缩ZIP文件&#xff0c;那么&#xff0c;如何合并zip分卷压缩包呢&#xff1f;今天我们分享两个ZIP分卷压缩包合并的方法给大家。 方法一&#xff1a; 我们可以将分卷压缩包&#xff0c;通过解压的方式…

E10:系统弹窗提示

效果– window.WeFormSDK.showMessage("这是一个E10的提示", 3, 2); const onClickCreate () > console.log("create"); const onClickSave () > console.log("save"); const onClickCancel () > dialogComponent?.destroy(); co…

Java四舍五入保留小数

这里介绍两种方法&#xff1a; package Book.jj.hh;import java.text.DecimalFormat; //使用DecimalFormat类 public class Demo1 {public static void main(String[] args) {double num 123.52631;DecimalFormat a new DecimalFormat("#.00"); //小数点后有几个0…

SpringCloud Gateway基础入门与使用实践总结

官网文档&#xff1a;点击查看官网文档 Cloud全家桶中有个很重要的组件就是网关&#xff0c;在1.x版本中都是采用的Zuul网关。但在2.x版本中&#xff0c;zuul的升级一直跳票&#xff0c;SpringCloud最后自己研发了一个网关替代Zuul&#xff0c;那就是SpringCloud Gateway一句话…

抖音账号永久封号后强制注销释放实名!一分钟教程方法公开

目前方法是可行的&#xff0c;不知道能保持多久&#xff01; 下载旧版本抖音&#xff1a;下载抖音6.8版本或5.8版本的老版本应用。 使用封禁手机号登录&#xff1a;使用已被永久封禁的手机号登录旧版本的抖音应用。 账号注销操作&#xff1a; 在设置中找到账号与安全的选项。…

从零开始发布你的第一个npm插件包并在多项目中使用

引言 在开源的世界里&#xff0c;每个人都有机会成为贡献者&#xff0c;甚至是创新的引领者。您是否有过这样的想法&#xff1a;开发一个解决特定问题的小工具&#xff0c;让她成为其他开发者手中的利器&#xff1f;今天&#xff0c;我们就来一场实战训练&#xff0c;学习如何将…

漏洞挖掘 | 验证码绕过

还是老规矩&#xff0c;开局一个登录框&#xff0c;中途漏洞全靠舔&#xff0c;先来研究一下这个登录窗口 很好&#xff0c;发现有验证码登录&#xff0c;先测试测试能不能并发 看来没有&#xff0c;只成功发送了两条&#xff0c;再看看验证码是不是4位 很好&#xff0c;是4位。…

UE5-AI

AI角色 角色控制器 AI角色必须要一个角色控制器 角色控制器最基本只需要执行行为树&#xff0c;在EventOnPossess后runBehaviorTree 如果要的是一个角色&#xff0c;可以创建一个Character&#xff0c;在类默认设置中可以找到 Pawn->AIControllerClass&#xff0c;在这里…

DSP问题:CCS更改工程名导入报错

1、问题现象 复制一个工程出来后&#xff0c;修改版本号&#xff0c;重新导入工程后报错。 显示项目描述无效。 2、问题原因 由于CCS无法通过工程描述中找到指定名字文件夹。使用记事本打开.project文件&#xff0c;里面的描述还是以前的文件夹名&#xff0c;所以导入时报…

Innodb Buffer Pool缓存机制(三)Innodb Buffer Pool内部组成

一、控制块缓存页 Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的&#xff0c;都是16KB。为了更好的管理这些在Buffer Pool中的缓存页&#xff0c;InnoDB为每一个缓存页都创建了一些所谓的控制信息&#xff0c;这些控制信息包括该页所属的表空间编号、页号、缓存…

[Vulfocus解题系列]spring 命令执行(CVE-2022-22947)

环境部署 使用docker部署环境 漏洞等级&#xff1a;高危 3 月 1 日&#xff0c;VMware 官方发布安全公告&#xff0c;声明对 Spring Cloud Gateway 中的一处命令注入漏洞进行了修复&#xff0c;漏洞编号为CVE-2022-22947 Spring官方发布 漏洞描述 使用 Spring Cloud Gate…

javaweb—Vue

重点为&#xff1a;双向数据绑定。 框架&#xff1a;是一个半成品软件&#xff0c;是一套可重用的、通用的、软件基础代码模型&#xff0c;基于框架进行开发&#xff0c;更加快捷&#xff0c;更加高效。 Vue快速入门 基础框架&#xff1a; <!DOCTYPE html> <html lan…

【Python Cookbook】S01E20 fnmatch 模块做字符串匹配

目录 问题解决方案讨论 问题 在不同的操作系统下&#xff0c;怎样做字符串匹配&#xff1f; 解决方案 fnmatch() 模块提供两个函数&#xff0c;fnmatch() 以及 fnmatchcase() 可以用来执行做这样的匹配。 from fnmatch import fnmatch, fnmatchcasematch_res fnmatch(foo.…

Technart电动螺丝刀TN101控制器维修

Technart电动螺丝刀以其高效、稳定和精确的扭矩控制而闻名。然而&#xff0c;即使优质的产品&#xff0c;在长时间的使用下&#xff0c;也可能会出现TECHNART电动螺母扳手控制器故障。 常见故障及维修方法 1. 控制器不工作 症状&#xff1a;电动螺丝刀无法启动&#xff0c;或启…

【WEEK15】 【DAY2】【DAY3】Email Tasks【English Version】

Continuation from【WEEK15】 【DAY1】Asynchronous Tasks【English Version】 Contents 17. Asynchronous, Timed, and Email Tasks17.2. Email Tasks17.2.1. Email sending is also very common in our daily development, and Springboot provides support for this as well…

JeecgBoot/SpringBoot升级Nacos(2.0.4到2.2.3)启动报错

错误如下&#xff1a; 报这种错误基本就很头大了&#xff0c;是框架不兼容的问题&#xff0c;自己找很难找到解决方法。 解决方案是把SpringBoot框架版本调高。 修改前&#xff1a; <parent><groupId>org.springframework.boot</groupId><artifactId&g…

Dell戴尔XPS 16 9640 Intel酷睿Ultra9处理器笔记本电脑原装出厂Windows11系统包,恢复原厂开箱状态oem预装系统

下载链接&#xff1a;https://pan.baidu.com/s/1j_sc8FW5x-ZreNrqvRhjmg?pwd5gk6 提取码&#xff1a;5gk6 戴尔原装系统自带网卡、显卡、声卡、蓝牙等所有硬件驱动、出厂主题壁纸、系统属性专属联机支持标志、系统属性专属LOGO标志、Office办公软件、MyDell、迈克菲等预装软…

Linux基础 (十四):socket网络编程

我们用户是处在应用层的&#xff0c;根据不同的场景和业务需求&#xff0c;传输层就要为我们应用层提供不同的传输协议&#xff0c;常见的就是TCP协议和UDP协议&#xff0c;二者各自有不同的特点&#xff0c;网络中的数据的传输其实就是两个进程间的通信&#xff0c;两个进程在…

32C3-2模组与乐鑫ESP32­-C3­-WROOM­-02模组原理图、升级口说明

模组原理图&#xff1a; 底板原理图&#xff1a; u1 是AT通信口&#xff0c;wiif-tx wifi-rx 是升级口&#xff0c;chip-pu是reset复位口&#xff0c;GPIO9拉低复位进入下载模式 ESP32-WROOM-32 系列硬件连接管脚分配 功能 ESP32 开发板/模组管脚 其它设备管脚 下载固件…

【Python报错】AttributeError: ‘NoneType‘ object has no attribute ‘xxx‘

成功解决“AttributeError: ‘NoneType’ object has no attribute ‘xxx’”错误的全面指南 一、引言 在Python编程中&#xff0c;AttributeError是一种常见的异常类型&#xff0c;它通常表示尝试访问对象没有的属性或方法。而当我们看到错误消息“AttributeError: ‘NoneTyp…