2024年11月

在各平台应用开发过程中,随着业务的功能增加,不免会涉及到非公开的
API
依赖,针对某些应用或厂商系统的适配,每个版本都需要投入精力去排查,
CoCollider
可以让我们的适配效率从几个星期提升到几小时即可完成。

项目已开源:

☞ Github:https://www.github.com/iofomo/fireyer ☜

☞ Github:https://www.github.com/iofomo/wing ☜

如果您也喜欢,别忘了给我们点个星。

01. 写在前面


Android
平台(后续会支持
iOS
平台)日常开发过程中,不免会接触到一些
Hidden
(或私有函数和类)的类,
JVM
层通过反射,
Native
则通过查找函数
symbol
来完成。特别是涉及系统
Framework
层和底层库较多的实现(特别是安全产品和系统工具类应用),成为每次系统和厂商
ROM
更新适配的重灾区,严重影响适配效率。

以往,开发同学要么先一个个自行排查代码后再提交测试,这个过程重复,耗时且容易遗漏。要么先让测试同学先进行用例覆盖测试,然后开发同学通过
BUG
来进行分析,这会导致整个团队效率更加低下。

由于我们虚拟化产品的特性,工程中涉及系统和应用适配的接口众多,工作量很大,主要有两部分:

  1. 如何快速寻找到系统新增的模块,感兴趣的可以查看我们的另一个开源项目
    【ASeeker】
    https://www.github.com/iofomo/aseeker
  2. 如何快速定位现有使用的
    API
    是否已发生变化,这就是
    【CoCollider】
    要解决的问题。

CoCollider
已经在我们内部使用,极大提升我们系统适配效率,原本需要几个星期的工作量,现在只需几小时便可适配完成,原理简单,堪称适配效率神器。
Android 15
发布在即,现在分享给大家。

CoCollider
同样适用于其他平台(如:iOS)和某些深度定制的应用版本适配。

02. 我们需要

对于系统的
API
,我们的需求是:

  1. 类的属性是否有变化。
  2. 类的成员属性是否有变化。
  3. 类的方法属性是否有变化。
  4. Native
    库和函数是否存在。

对于开发工程师的工作量,首次需要在源码中添加注释标签,这个工作不可省略,却一劳永逸。后续需要在每次涉及适配的代码处加上
@CoCollider
标签就行。

Java代码标签格式:

格式:
// @CoCollider {class name},{-/+}{field name/method name},...

范例:
# 仅查看类是否变更
// @CoCollider android.utils.Abc
# 仅查看类所有成员是否变更
// @CoCollider android.utils.Abc,-*
# 查看类和某成员或方法是否变更(支持多个)
// @CoCollider android.utils.Abc,-mFile
// @CoCollider android.utils.Abc,-mFile,mName
// @CoCollider android/utils/Abc,-mFile
// @CoCollider android.utils.Abc,+getFile,-mName
// @CoCollider android/utils/Abc,+getFile,+getName,-mName

# 支持缺省自动填充:(按照顺序依次填充)
Class.forName("android.utils.Abc");// @CoCollider
# 等同于:// @CoCollider android.utils.Abc

ReflectUtils.getStaticField("android.utils.Abc", "mName");// @CoCollider ,-
# 等同于:// @CoCollider android.utils.Abc,-mName

Native 代码标签格式:

格式:
// @CoCollider ~{lib name},{-/+}{field name/method name},...
  
范例:
# 查看方法是否变更
// @CoCollider ~libc.so,+open
// @CoCollider ~/system/lib/libc.so,+open
// @CoCollider ~/system/lib/libc.so,+open,+close

# 支持缺省自动填充:(按照顺序依次填充)
utils_dlsym("libc.so", "open");// @CoCollider ~,+
# 等同于:// @CoCollider ~libc.so,+open

utils_dlsym("open");// @CoCollider ~libc.so,+
# 等同于:// @CoCollider ~libc.so,+open

03. 配置运行

  1. 电脑下载配置
    wing

  2. 手机安装
    Fireyer
    ,或集成
    CoCollider
    模块的应用,链接
    adb

  3. 扫描代码:

    # 命令格式:扫描当前目录下的代码(默认支持 h/c/cpp/java/kt/aidl,也可以追加文件类型)
    $ wing -cocollider scan
    $ wing -cocollider scan /home/space
    # 或
    $ python ./cocollider.py scan
    $ python ./cocollider.py scan /home/space
    # 在当前目录输出结果
    >>> cocollider-scan-20241023-112044.txt
    
  4. 解析运行:

    # 命令格式:调用 Fireyer 应用
    $ wing -cocollider run /home/cocollider-scan-20241023-112044.txt
    # 或
    $ python ./cocollider.py run /home/cocollider-scan-20241023-112044.txt
    
    # 在当前目录输出结果
    >>> cocollider-run-20241023-112044.txt
    
  5. 使用对比工具查看结果即可快速定位变更内容。

04. 扫描结果

cocollider-scan-20241023-112044.txt
中的文件内容为:

############################################################
= android.utils.Abc

- mFile
> application/fireyer/test/test1.java,14

- mName3
> application/fireyer/test/test1.java,14

- mPath
> application/fireyer/test/test1.java,14

############################################################
= android.utils.Abc1

- mFile
> application/fireyer/test/test1.java,5

...

05. 运行结果

cocollider-run-20241023-112044.txt
中的文件内容(已对
class

lib
库,
function

field

method
排序,便于使用对比工具对比)

############################################################
~ libwilhelm.so
[OK]

+ _ZN7androidxx6BufferE
[OK], public final class libcore.io.Linux
> cmpt/xxx/jni/xxx/jni/src/xxx.cpp,362

+ _ZN7android1xx6BufferE
[OK], public final class libcore.io.Linux
> cmpt/xxx/jni/xxx/jni/src/xxx.cpp,350


############################################################
= ohos.abilityshell.HarmonyApplication
[Fail]

- applicationHandler
[Fail]
> scene/xxx/xxx.java,36


############################################################
= ohos.system.Parameters
[Fail]

+ nativeGet
[Fail]
> scene/xxx.cpp,250

...

06. 对比结果

分别在不同版本系统运行
python cocollider.py run
之后,使用对比工具(如:
Beyond Compare
)对内容进行比较查看,可以快速找到新增,修改和删除项,从而可以快速进行排查和修复。

前言

对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。

在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

确实,spring事务用起来贼爽,就用一个简单的注解:
@Transactional
,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。

但如果你使用不当,它也会坑你于无形。

今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。不信,让我们一起看看。

一 事务不生效

1.访问权限问题

众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

我们可以看到add方法的访问权限被定义成了
private
,这样会导致事务失效,spring要求被代理方法必须是
public
的。

说白了,在
AbstractFallbackTransactionAttributeSource
类的
computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则
TransactionAttribute
返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是
public
,而是private、default或protected的话,spring则不会提供事务功能。

2. 方法用final修饰

有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到add方法被定义成了
final
的,这样会导致事务失效。

为什么?

如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

3.方法内部调用

有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    //@Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

3.1 新加一个Service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }

 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

 }

3.2 在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

答案:不会。

其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。但有些坑,如果你想进一步了解循环依赖问题,可以看看我之前文章《
spring:我是如何解决循环依赖的?
》。

3.3 通过AopContent类

在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

4.未被spring管理

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

当然创建bean实例的方法还有很多,有兴趣的小伙伴可以看看我之前写的另一篇文章《
@Autowired的这些骚操作,你都知道吗?

如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如:

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

从上面的例子,我们可以看到UserService类没有加
@Service
注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。

5.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

6.表不支持事务

周所周知,在mysql5之前,默认的数据库引擎是
myisam

它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。

有些老项目中,可能还在用它。

在创建表的时候,只需要把
ENGINE
参数设置成
MyISAM
即可:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

myisam好用,但有个很致命的问题是:
不支持事务

如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。

此外,myisam还不支持行锁和外键。

所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。

有时候我们在开发的过程中,发现某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务。

7.未开启事务

有时候,事务没有生效的根本原因是没有开启事务。

你看到这句话可能会觉得好笑。

开启事务不是一个项目中,最最最基本的功能吗?

为什么还会没有开启事务?

没错,如果项目已经搭建好了,事务功能肯定是有的。

但如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?

当然原因有很多,但没有开启事务,这个原因极其容易被忽略。

如果你使用的是springboot项目,那么你很幸运。因为springboot通过
DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。

你所要做的事情很简单,只需要配置
spring.datasource
相关参数即可。

但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。

具体配置如下信息:

   
<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

默默的说一句,如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。

二 事务不回滚

1.错误的传播特性

其实,我们在使用
@Transactional
注解时,是可以指定
propagation
参数的。

该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:

  • REQUIRED
    如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  • SUPPORTS
    如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY
    如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW
    每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED
    如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER
    如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED
    如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

2.自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如:

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

3.手动抛了别的异常

即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

因为spring事务,默认情况下只会回滚
RuntimeException
(运行时异常)和
Error
(错误),对于普通的Exception(非运行时异常),它不会回滚。

4.自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置
rollbackFor
参数,来完成这个功能。

但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

这是为什么呢?

因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。

5.嵌套事务回滚多了

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

why?

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

三 其他

1 大事务问题

在使用spring事务时,有个让人非常头疼的问题,就是大事务问题。

通常情况下,我们会在方法上
@Transactional
注解,填加事务功能,比如:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}


@Transactional
注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

上面的这个例子中,在UserService类中,其实只有这两行才需要事务:

roleService.save(userModel);
update(userModel);

在RoleService类中,只有这一行需要事务:

saveData(userModel);

现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。

如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

关于大事务问题的危害,可以阅读一下我的另一篇文章《
让人头痛的大事务问题到底要如何解决?
》,上面有详细的讲解。

2.编程式事务

上面聊的这些内容都是基于
@Transactional
注解的,主要说的是它的事务问题,我们把这种事务叫做:
声明式事务

其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:
编程式事务
。例如:


   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。

相较于
@Transactional
注解声明式事务,我更建议大家使用,基于
TransactionTemplate
的编程式事务。主要原因如下:

  1. 避免由于spring aop问题,导致事务失效的问题。
  2. 能够更小粒度的控制事务的范围,更直观。

建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

上周赠送了
OneThingAI 算力代金券

天翼云电脑
,这周继续给园子会员送福利。

这周赠送的是一年华为云服务器,配置是2核2G3M带宽,限量100台,先到先得,领完为止。

领取要求:博客园年度VIP或者PLUS
会员
,华为云新用户或者在华为云没有过任何消费的老用户。

领取方式:加园子
企业微信
,加好友时请备注【领华为云服务器】,或者给「博客园团队」企微发消息。

如果符合领取条件,会将您加入专门领华为云服务器的微信群,群中有一个Wod文档,按照文档中的流程完成领取,领取过程中遇到任何问题可以在群中咨询。

负责发放云服务器的是华为云服务商浙江杭云网络科技有限公司,也是园子的合作伙伴,园子的
华为云代理业务
就是和他们合作的,如果您有华为云云产品购买需求,也欢迎和我们联系。

Chrome 130 版本新特性& Chrome 130 版本发行说明

一、Chrome 130 版本浏览器更新

1. 新的桌面提示

Chrome 130 引入了一种新的 Toast 样式,用于在用户操作后提供视觉确认,或快速提供后续操作的途径。

当用户执行某个操作时,Toast 会在屏幕上短暂弹出,确认操作成功或提供快捷链接。
例如,当将某项内容添加到阅读列表时,Toast 会确认项目已添加,并提供一个快速链接以打开阅读列表侧边栏。Toast 以小型提示的形式显示,部分覆盖网页内容,部分覆盖浏览器顶部工具栏。

image

2. 用于 macOS 系统的屏幕共享平台选择器

在 macOS X Sequoia 上的 Chrome 中共享屏幕时,用户现在可以使用更新后的平台选择器选择要共享的窗口或屏幕。此新平台选择器无需为 Chrome 分配屏幕录制权限,并且与其他 macOS 应用程序中的屏幕共享一致。

新的选择器将不会在 macOS Sequoia 的第一次更新之前启用,预计 15.1 版本将在 15.0 初始版本发布后一个月内发布。在此之前,Chrome 用户可能会看到一个警告对话框,提示 Chrome 尚未使用新的选择器 API。

image

3.
新帐户菜单

一些用户现在可以通过在新标签页上点击他们的头像来访问新的账户菜单。新的账户菜单允许他们轻松地注销、切换账户以及解决与 Chrome 账户相关的错误。

3.1. IOS 上的 130

image

4.
Android 上的 PDF 查看器

此功能提供了在 Chrome 浏览器 UI 中查看 PDF 的功能。

使用此功能,PDF 将在 Chrome 中无缝呈现。用户仍然可以下载 PDF 并使用其他首选或第三方应用打开。

5.
节能模式下冻结标签

image

当节能模式激活时,Chrome 现在会冻结已隐藏且静默超过 5 分钟且占用大量 CPU 的标签。

这将延长电池寿命并通过减少 CPU 使用率来加快 Chrome 的速度。

5.1. 特殊标签除外

  1. 改标签有音频或视频会议功能
  2. 改标签控制外部设备(使用 Web USB、Web 蓝牙、Web HID 等)

6. 共享 Brotli 和共享 Zstandard 的压缩字典传输

功能增加了使用指定的先前响应作为外部字典,以 Brotli 或 Zstandard 对内容编码的响应进行压缩的支持。

7. 键盘可聚焦滚动容器

Chrome 130 通过使滚动容器在顺序焦点导航中可聚焦,改进了可访问性。目前,Tab 键不会聚焦滚动容器,除非
tabIndex
被显式设置为 0 或更大。

现在,默认情况下滚动容器可聚焦,这样无法(或不想)使用鼠标的用户可以通过 Tab 键和方向键来聚焦被裁剪的内容。只有当滚动容器中没有可通过键盘聚焦的子元素时,此行为才会启用。这样做是为了避免对包含可聚焦元素(如
<textarea>
)的滚动容器造成兼容性问题。

8. 支持非特殊的 URL

Chrome 130 支持非特殊 scheme 的 URL,例如
git://example.com/path
。此前,Chromium 的 URL 解析器不支持非特殊 URL,且将这些 URL 解析为不透明路径(opaque path),这与 URL 标准不一致。现在,Chromium 的 URL 解析器能够正确解析非特殊 URL,符合 URL 标准。

9. Android 版 Chrome 支持第三方自动填充和密码提供商

在 Chrome 130 中,添加了对 Android 自动填充的直接支持,这意味着这些提供商现在可以在 Chrome 的 Android 版本中正常工作,无需依赖无障碍 API。这将提高 Chrome 在 Android 上的性能。要利用此功能,用户需要确保在 Android 设置中配置了第三方提供商。然后,在 Chrome 中,用户需要打开设置 > 自动填充服务,并选择使用其他服务进行自动填充。

新设置将在 Chrome 130 中可用。如果用户使用新的设置,将立即生效。如果不使用新的设置,用户将继续通过无障碍功能使用 Google 和第三方提供商(如果已安装)。

10.
<meter>
元素的后备样式

在 Chrome 130 中,具有
appearance: none

<meter>
元素现在有了合理的后备样式,匹配 Safari 和 Firefox,而不是仅仅从页面上消失。此外,开发者现在可以自定义样式
<meter>
元素。

11. Chrome 浏览器中的新政策和更新政策

策略 描述
DataURLWhitespacePreservationEnabled 所有媒体类型的 DataURL 空白字符保留
CloudProfileReportingEnabled 为托管配置文件启用 Google Chrome 云报告

二、ChromeOS 130 版本更新

1. 快速插入

快速插入提供了一种快速插入表情符号、符号、GIF、Google Drive 链接以及快速计算和单位转换的方法,可以通过键盘按键(在部分型号上)或键盘快捷键实现。

在 ChromeOS 130 中,所有 ChromeOS 设备都可以使用新的快捷键 Launcher + f。新的硬件按键最初仅在三星 Galaxy Chromebook Plus 上提供,但快速插入键将在 2025 年推出,适用于更多设备。

image

2. 设置和快捷键更改

更新了设置中的快捷键和输入设备选项,包括:

  • 快速插入:Launcher + f

3. 专注于 ChromeOS

设计了“专注于 ChromeOS”功能,以帮助用户减少干扰,创建更高效的工作空间。通过“专注”,可以轻松设置和调整专注时间,启用或禁用请勿打扰(DND)模式,整理或创建新的 Google 任务,并沉浸在精选的播放列表中,这些播放列表可以帮助更好地专注,包括专注音效等。

要使用“专注”,请前往

Quick Settings > Focus

image

4. 增强的 Drive 文件访问

除了在 Tote 中标记的文件之外,还可以直接从架子上访问所有标记的 Drive 文件,现已支持离线访问。

5. Tote 中的新建议

通过本地和 Drive 文件建议,快速访问和固定最需要的文件。Tote 中的新建议部分会向用户推荐文件,提高了那些对用户有用的文件的可见性,以便他们固定并离线访问。

6. 欢迎回顾

新的欢迎回顾功能帮助用户在启动时恢复工作并探索新选项。一旦启用此功能,将能够预览并恢复上次会话中的应用和标签页。欢迎回顾还提供有用的信息,如天气、下一个日历事件、来自其他设备的最近标签页以及相关的 Google Drive 建议。

要启用此功能,请选择
Settings > System Preferences > Startup > Welcome Recap
,并确保为设备选择“
Ask every time
”。

设置 > 系统偏好 > 启动 > 欢迎回顾

image

7. 工作室风格麦克风

通过在视频通话控制中启用此功能,使 Chromebook 的内置麦克风听起来像专业的工作室麦克风。工作室风格麦克风包括现有的噪声取消和去混响效果,并通过先进的平衡、细节重建和房间适应进一步增强这些效果。启用噪声取消的用户将默认获得工作室风格麦克风的增强效果,从此版本开始。如果用户想恢复到旧的仅噪声取消效果,可以在
Settings > Device > Audio
中选择相应的选项。此功能仅适用于 Chromebook Plus 设备。

设置 > 设备 > 音频

8. AI 驱动的录音应用

ChromeOS 130 引入了新的 Google AI 驱动的录音应用,可创建转录,能够检测和标记说话者,并提供录音内容的摘要。不仅限于录音,还提供语音转文本、内容摘要和标题建议,均由 Google AI 提供支持。

9. 内容扫描用于托管访客会话

现在允许组织将 Chrome Enterprise Premium 强大的扫描和基于内容和上下文的保护扩展到 ChromeOS 上托管访客会话中的本地文件。例如,当用户尝试将包含社会安全号码的错误文件复制到外部驱动器时,该文件会立即被阻止,从而保护这一机密信息。

10. 在 Kiosk 模式中允许额外的 URL

如果 Kiosk 应用使用多个 URL 源,IT 管理员现在可以输入额外的源。所有指定的源将自动获得权限。任何不在此列表中的其他源将被拒绝权限。

image

11. 外观效果

外观效果在相机、虚拟会议和短视频产品中长期以来一直很受欢迎,并已在一些 Google 产品中推出。在 ChromeOS 130 中,将此功能集成到 Chromebook 的视频通话控制中。仅在 Chromebook Plus 设备上可用。

12. 更加可访问的隐私控制

在此次发布中,使 Chrome 浏览器的操作系统级隐私控制对用户更易获取。旨在让用户更加意识到,要使摄像头或麦克风工作,他们需要启用操作系统级的隐私控制。

image

13. 增强的键盘亮度控制

Chromebook 用户现在可以直接从设置应用轻松调整键盘亮度和控制环境光传感器。这个新功能允许将键盘亮度设置到合适的水平,并根据需要开启或关闭环境光传感器。

14. 增强的显示亮度控制

Chromebook 用户现在可以直接从设置应用轻松调整显示亮度和控制环境光传感器。这个新功能允许在设置中将屏幕亮度设置到合适的水平,并根据需要开启或关闭环境光传感器。

15. 在 ChromeOS 上帮助我阅读

在 ChromeOS 上帮助我阅读提供了一个 AI 驱动的解决方案,帮助快速找到任何文本中所需的信息。只需右键单击空白区域,即可在现有上下文菜单上方显示“帮助我阅读”卡片,轻松获取在浏览器和图库中阅读内容的要点。帮助我阅读面板展示了文本的摘要和一个自由问答字段,允许询问有关文本的具体问题。仅在 Chromebook Plus 设备上可用。

image

16. 多日历支持

新版本推出了多日历支持,允许用户查看他们在 Google 日历中选择的多个日历中的所有事件。

image

17. 画中画窗口

ChromeOS 用户现在可以享受更大的灵活性,使用画中画(PiP)窗口。PiP Tuck 允许用户将 PiP 窗口临时移动到屏幕的一侧,腾出宝贵的屏幕空间,同时保持视频的便捷访问。此外,可以通过快速双击轻松调整 PiP 窗口的大小,在两种尺寸之间切换,以获得最佳观看体验。

image

18. 改进的 ARC++ 用户体验

为了改善 ChromeOS 和 ARC++ 的用户体验,将 ARC++ 的非紧急后台和错误通知移至系统托盘。这可以防止这些消息不必要地弹出在前台,从而打扰用户的使用体验。

19. 新的策略以控制接入点名称

对于具有蜂窝功能的 Chromebook,接入点名称(APN)策略允许管理员限制自定义 APN 的使用。通过在一般网络设置中将 AllowAPNModification 标志设置为限制,他们可以防止最终用户添加或使用任何自定义 APN。

image

20. Microsoft SCEP SID 更新

仅适用于使用 Microsoft NPS 进行 RADIUS 的 SCEP 部署。如果没有将 SCEP 证书与 Microsoft NPS 结合使用于 Chromebook 网络连接,则可以忽略这些指令的其余部分。

三、Chrome 130 版本更新日期

1. Chrome 130

1.1. Beta 版

2024 年 9 月 18 日,星期三

1.2. 稳定版本

2024 年 10 月 15 日,星期二

2. Chrome操作系统

2.1. Beta 版

2024 年 10 月 1 日,星期二

2.2. 稳定版本

2024 年 10 月 29 日,星期二

参考资料

这两个滤波器也是很久前就看过的,最近偶然翻起那本比较经典的matlab数字图像处理(冈萨雷斯)书,里面也提到了这个算法,觉得效果还行,于是也还是稍微整理下。

为了自己随时翻资料舒服和省事省时,这个算法的原理我们还是把他从别人的博客里搬过来吧:

摘自:
图像处理基础(2):自适应中值滤波器(基于OpenCV实现)

自适应的中值滤波器也需要一个矩形的窗口S
xy
,和常规中值滤波器不同的是这个窗口的大小会在滤波处理的过程中进行改变(增大)。需要注意的是,滤波器的输出是一个像素值,该值用来替换点(x,y)处的像素值,点(x,y)是滤波窗口的中心位置。

原理说明

过程
A的目的是确定当前窗口内得到中值
Zmed是否是噪声。如果Z
min
<Z
med
<Z
max
,则中值Z
med
不是噪声,这时转到过程
B测试,当前窗口的中心位置的像素
Zxy是否是一个噪声点。如果Z
min
<Z
xy
<Z
max
,则Z
xy
不是一个噪声,此时滤波器输出Z
xy
;如果不满足上述条件,则可判定Zxy是噪声,这是输出中值Z
med
(在
A中已经判断出
Z
med
不是噪声)。

如果在过程
A中,得到则
Z
med
不符合条件Z
min
<Z
med
<Z
max
,则可判断得到的中值Z
med
是一个噪声。在这种情况下,需要增大滤波器的窗口尺寸,在一个更大的范围内寻找一个非噪声点的中值,直到找到一个非噪声的中值,跳转到
B;或者,窗口的尺寸达到了最大值,这时返回找到的中值,退出。

从上面分析可知,噪声出现的概率较低,自适应中值滤波器可以较快的得出结果,不需要去增加窗口的尺寸;反之,噪声的出现的概率较高,则需要增大滤波器的窗口尺寸,这也符合种中值滤波器的特点:噪声点比较多时,需要更大的滤波器窗口尺寸。

摘抄完成..............................................

这个过程理解起来也不是很困难,
图像处理基础(2):自适应中值滤波器(基于OpenCV实现)
这个博客也给出了参考代码,不过我很遗憾的告诉大家,这个博客的效果虽然可以,但是编码和很多其他的博客一样,是存在问题的。

核心在这里:

for (int j = maxSize / 2; j < im1.rows - maxSize / 2; j++)
{
for (int i = maxSize / 2; i < im1.cols * im1.channels() - maxSize / 2; i++)
{
im1.at
<uchar>(j, i) =adaptiveProcess(im1, j, i, minSize, maxSize);
}
}

他这里的就求值始终是对同一份图,这样后续的处理时涉及到的领域实际上前面的部分已经是被修改的了,不符合真正的原理的。 至于为什么最后的结果还比较合适,那是因为这里的领域相关性不是特别强。

我这里借助于最大值和最小值滤波以及中值滤波,一个简单的实现如下所示:

/// <summary>
///实现图像的自使用中值模糊。更新时间2015.3.11。///参考:Adaptive Median Filtering Seminar Report By: PENG Lei (ID: 03090345)/// </summary>
/// <param name="Src">需要处理的源图像的数据结构。</param>
/// <param name="Dest">保存处理后的图像的数据结构。</param>
/// <param name="Radius">滤波的半径,有效范围[1,127]。</param>
/// <remarks>1: 能处理8位灰度和24位及32位图像。</remarks>
/// <remarks>2: 半径大于10以后基本没区别了。</remarks>
int IM_AdaptiveMedianFilter(unsigned char* Src, unsigned char* Dest, int Width, int Height, int Stride, int MinRadius, intMaxRadius)
{
int Channel = Stride /Width;if ((Src == NULL) || (Dest == NULL)) returnIM_STATUS_NULLREFRENCE;if ((Width <= 0) || (Height <= 0)) returnIM_STATUS_INVALIDPARAMETER;if ((MinRadius <= 0) || (MaxRadius <= 0)) returnIM_STATUS_INVALIDPARAMETER;if ((Channel != 1) && (Channel != 3)) returnIM_STATUS_NOTSUPPORTED;int Status =IM_STATUS_OK;int Threshold = 10;if (MinRadius >MaxRadius) IM_Swap(MinRadius, MaxRadius);bool AllProcessed = false;
unsigned
char* MinValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* MaxValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* MedianValue = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
unsigned
char* Flag = (unsigned char*)malloc(Height * Width * sizeof(unsigned char));if ((MinValue == NULL) || (MaxValue == NULL) || (MedianValue == NULL) || (Flag ==NULL))
{
Status
=IM_STATUS_OUTOFMEMORY;gotoFreeMemory;
}
memset(Flag,
0, Height * Width * sizeof(unsigned char));if (Channel == 1)
{
//The median filter starts at size 3-by-3 and iterates up to size MaxRadius-by-MaxRadius
for (int Z = MinRadius; Z <= MaxRadius; Z++)
{
Status
=IM_MinFilter(Src, MinValue, Width, Height, Stride, Z);if (Status != IM_STATUS_OK) gotoFreeMemory;
Status
=IM_MaxFilter(Src, MaxValue, Width, Height, Stride, Z);if (Status != IM_STATUS_OK) gotoFreeMemory;
Status
= IM_MedianBlur(Src, MedianValue, Width, Height, Stride, Z, 50);if (Status != IM_STATUS_OK) gotoFreeMemory;for (int Y = 0; Y < Height; Y++)
{
int Index = Y *Stride;int Pos = Y *Width;for (int X = 0; X < Width; X++, Index++, Pos++)
{
if (Flag[Pos] == 0)
{
int Min = MinValue[Index], Max = MaxValue[Index], Median =MedianValue[Index];if ((Median > Min + Threshold) && (Median < Max -Threshold))
{
int Value =Src[Index];if ((Value > Min + Threshold) && (Value < Max-Threshold))
{
Dest[Index]
=Value;
}
else{
Dest[Index]
=Median;
}
Flag[Pos]
= 1;
}
}
}
}
AllProcessed
= true;for (int Y = 0; Y < Height; Y++)
{
int Pos = Y *Width;for (int X = 0; X < Width; X++)
{
if (Flag[Pos + X] == 0) //检测是否每个点都已经处理好了
{
AllProcessed
= false;break;
}
}
if (AllProcessed == false) break;
}
if (AllProcessed == true) break;
}
/*Output zmed for any remaining unprocessed pixels. Note that this
zmed was computed using a window of size Smax-by-Smax, which is
the final value of k in the loop.
*/

if (AllProcessed == false)
{
for (int Y = 0; Y < Height; Y++)
{
int Index = Y *Stride;int Pos = Y *Width;for (int X = 0; X < Width; X++)
{
if (Flag[Pos + X] == 0) Dest[Index + X] = Src[Index +X];
}
}
}
}
else{

}
FreeMemory:
if (MinValue !=NULL) free(MinValue);if (MaxValue !=NULL) free(MaxValue);if (MedianValue !=NULL) free(MedianValue);if (Flag !=NULL) free(Flag);returnStatus;
}


注意这里,我们还做了适当的修改,增加了一个控制阈值Threshold,把原先的
if ((Median > Min) && (Median < Max))

修改为:


if ((Median > Min + Threshold) && (Median < Max - Threshold))


也可以说是对应用场景的一种扩展,增加了函数的韧性。


当我们的噪音比较少时,这个函数会很快收敛,也就是不需要进行多次的计算的。


另外还有一个算法叫Conservative Smoothing,翻译成中文可以称之为保守滤波,这个算法在
https://homepages.inf.ed.ac.uk/rbf/HIPR2/csmooth.htm
有较为详细的说明,其基本原理是:

 This is accomplished by a procedure which first finds the minimum and maximum intensity values of all the pixels within a windowed region around the pixel in question. If the intensity of the central pixel lies within the intensity range
spread of its neighbors, it is passed on to the output image unchanged. However, if the central pixel intensity is greater than the maximum value, it is set equal to the maximum value; if the central pixel intensity is less than the minimum value,
it is set equal to the minimum value. Figure 1 illustrates this idea.


  

比如上图的3*3领域,除去中心点之外的8个点其最大值为127,最小值是115,而中心点的值150大于这个最大值,所以中心点的值会被修改为127。

注意这里和前面的自适应中值滤波有一些不同。

1、虽然他也利用到了领域的最大值和最小值,但是这个领域是不包含中心像素本身的,这个和自适应中值是最大的区别。

2、这个算法在满足某个条件时,不是用中值代替原始像素,而是用前面的改造的最大值或者最小值。

3、这个算法也可以改造成和自适应中值一样,不断的扩大半径。

4、对于同一个半径,这个函数多次迭代效果不会有区别。

一个简单的实现如下:

    for (int X = 0; X < Width * Channel; X++, LinePD++)
{
int P0 = First[X], P1 = First[X + Channel], P2 = First[X + 2 *Channel];int P3 = Second[X], P4 = Second[X + Channel], P5 = Second[X + 2 *Channel];int P6 = Third[X], P7 = Third[X + Channel], P8 = Third[X + 2 *Channel];int Max0 =IM_Max(P0, P1);int Min0 =IM_Min(P0, P1);int Max1 =IM_Max(P2, P3);int Min1 =IM_Min(P2, P3);int Max2 =IM_Max(P5, P6);int Min2 =IM_Min(P5, P6);int Max3 =IM_Max(P7, P8);int Min3 =IM_Min(P7, P8);int Max =IM_Max(IM_Max(Max0, Max1), IM_Max(Max2, Max3));int Min =IM_Min(IM_Min(Min0, Min1), IM_Min(Min2, Min3));if (P4 >Max)
P4
=Max;else if (P4 <Min)
P4
=Min;
LinePD[
0] =P4;
}

因为去除了中心点后进行的最大值和最小计算,这个算法如果要实现高效率的和半径无关的版本,还是需要做一番研究的,目前我尚没有做出成果。

我们拿标准的Lena图测试,使用matlab分别为他增加0.02及0.2范围的椒盐噪音,然后使用自适应中值滤波及保守滤波进行处理。



添加0.02概率的椒盐噪音图                         最小半径1,最大半径13时的去噪效果                          3*3的保守滤波



添加0.2概率的椒盐噪音图                              最小半径1,最大半径5时的去噪效果                                3*3的保守滤波

可以看到,自适应中值滤波在去除椒盐噪音的效果简直就是逆天,基本完美的复现了原图,有的时候我自己都不敢相信这个结果。而保守滤波由于目前我只实现了3*3的版本,因此对于噪音比较集中的地方暂时无法去除,这个可以通过扩大半径和类似使用自适应中值的方式处理,而这种噪音的图像使用DCT去噪、小波去噪、NLM等等都不具有很好的效果,唯一能和他比拟的就只有蒙版和划痕里面小参数时的效果(这也是以中值为基础的)。所以去除椒盐噪音还是得靠中值相关的算法啊。

和常规的中值滤波器相比,自适应中值滤波器能够更好的保护图像中的边缘细节部分,当然代价就是增加了算法的时间复杂度。