`
KuangYeYaZi
  • 浏览: 56267 次
文章分类
社区版块
存档分类
最新评论

spring事务支持

 
阅读更多

       本文主要探讨如何利用Spring来装配组件,包括其事务上下文。从J2EE应用程序内部连接到单个的数据库并不是什么难事。但是,如果要装配或者集成企业级的组件,情况就复杂了。一个组件可以有一个或多个支持它的数据库,因此,当装配两个或更多的组件时,我们希望能够保持在跨组件的多个数据库中进行的操作的原子性。J2EE服务器为这些组件提供了一个容器来保证事务原子性和跨组件独立性。如果使用的不是J2EE服务器,则可以利用Spring来帮助我们。Spring基于Inversion of Control(控制反转)模式(也称为依赖注入),它不仅可以连接组件服务,还可以连接关联的事务上下文。在本文中,我们将Hibernate用作对象/关系持久性存储和查询服务。

  

  装配组件事务

  假设在企业组件库里,我们已经有一个审计组件,里面有可以被客户端调用的服务方法。然后,当我们想要构建一个订单处理系统时,我们发现存在这样的设计要求:OrderListManager组件服务同样需要审计组件服务。OrderListManager创建和管理订单,因此所有的OrderListManager服务都有自己的事务属性。当我们从OrderListManager服务内调用审计组件时,我们实际上是在把OrderListManager服务的事务上下文传播给审计服务。也许将来新的业务服务组件同样需要审计组件,但那时将在一个不同的事务上下文中调用它。实际结果就是,即使审计组件的功能保持不变,它也可能是由别的业务服务功能组成,包含了混搭的(mix-and-match)事务属性来提供不同的运行时事务性行为。

  在图1中有两个独立的调用上下文流程。在流程1里,如果客户端有TX上下文,那么OrderListManager既可以参与其中,也可以启动一个新的TX,这取决于客户端是否在TX中,以及为OrderListManager方法指定了什么样的TX属性。这同样适用于OrderListManager服务依次调用AuditManager方法的情况。

  图1 装配组件事务

  EJB架构允许组件装配者声明式地给出正确的事务属性,从而为他们提供这种灵活性。我们不探讨声明式事务管理的替代方案(即所谓的编程式事务控制),因为这会牵涉到代码更改,从而产生不同的运行时事务行为。几乎所有的J2EE应用服务器都按照X/Open XA规范提供了服从两阶段提交协议的分布式事务管理器。现在的问题是,我们能不能利用EJB服务器来实现相同的功能?Spring就是其中的一种解决方案。让我们来看一下Spring如何帮助我们解决事务组装的问题:

  使用Spring进行事务管理

  我们将看到一个轻量级的事务基础架构,它实际上可以管理组件级的事务装配。Spring是其中的一个解决方案。它的优点在于,我们不会被捆绑到J2EE容器服务(如JNDI DataSource)上。最棒的一点是,如果我们想把这个轻量级事务基础架构关联到一个已可用的J2EE容器基础架构,将不会有任何问题。看起来我们可以利用两者的优点。

  另一方面,Spring这个轻量级事务基础架构使用了一个面向方面编程(Aspect-Oriented Programming,AOP)框架。Spring AOP框架使用了一个支持AOP的Spring bean工厂。在特定于Spring的配置文件applicationContext.xml中,通过在组件服务级指定事务特性来划分事务。

<beans>
<!-- other code goes here... -->
<bean id="orderListManager"
        class="org.springframework.transaction
        .interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
        <ref local="transactionManager1"/>
</property>
<property name="target">
        <ref local="orderListManagerTarget"/>
</property>
<property name="transactionAttributes">
        <props>
                <prop key="getAllOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="getOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="createOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="addLineItem">
                   PROPAGATION_REQUIRED,
                   -com.example.exception.FacadeException
                </prop>
                <prop key="getAllLineItems">
                        PROPAGATION_REQUIRED,readOnly
                </prop>
                <prop key="queryNumberOfLineItems">
                        PROPAGATION_REQUIRED,readOnly
                </prop>
        </props>
</property>
</bean>
</beans>

 

  一旦我们在服务级指定了事务属性,org.springframework.transaction.PlatformTransactionManager接口的一个特定实现就会截获并解释它们。该接口如下:

public interface PlatformTransactionManager{
        TransactionStatus getTransaction
                (TransactionDefinition definition);
        void commit(TransactionStatus status);
        void rollback(TransactionStatus status);
}

 

  Hibernate事务管理器

  由于我们已决定使用Hibernate作为ORM工具,下一步要做的就是配置一个特定于Hibernate的事务管理器实现。

<beans>
<!-- other code goes here... -->
<bean id="transactionManager1"
        class="org.springframework.orm.hibernate.
                HibernateTransactionManager">
        <property name="sessionFactory">
                <ref local="sessionFactory1"/>
        </property>
</bean>
</beans>

 

  设计多个组件中的事务的管理

  现在,我们来讨论什么是“装配组件事务”。您也许注意到了为域中的服务级组件OrderListManager所指定的各种TX属性。图2所示的业务域对象模型(Business Domain Object Model,BDOM)显示了我们的域所确定的主要对象:

在Spring中配置Hibernate的事务(图二)
图2 业务域对象模型(BDOM)

  图字:Order:订单;Audit:审计

  为了更好的说明,我们来列出我们的域中的一些非功能性需求(Non-Functional Requirement,NFR):

  业务对象需要保存在一个数据库中(appfuse1)。
审计时要登录到另一个数据库中(appfuse2),出于安全的考虑,数据库要有防火墙保护。
业务组件应该可以重用。
必须尽一切努力审计业务服务层的所有活动。

  考虑了以上要求之后,我们决定,OrderListManager服务会将所有的审计日志调用委托给已经可用的AuditManager组件。这样就得出了详细设计,如图3所示:

在Spring中配置Hibernate的事务(图三)
图3 组件服务的设计

  这里值得注意的一点是,由于我们的NFR,我们要将与OrderListManager相关的对象映射到appfuse1数据库,而将与审计相关的对象映射到appfuse2。这样,无论要审计什么,OrderListManager组件都会调用AuditManager组件。我们会看到,OrderListManager组件中的所有方法都应该是事务性的,因为我们通过服务来创建订单和线项目(line item)。那么AuditManager组件中的服务呢?因为它做的是审计跟踪,我们关心的是尽可能维持长时间的审计跟踪,并针对系统中所有可能的业务活动。这就产生了如下的需求:“即使主要的业务活动失败了,也要进行审计跟踪记录”。AuditManager组件同样要有自己的事务,因为它也与自己的数据库进行交互。如下所示:

<beans>
<!-- other code goes here... -->
<bean id="auditManager"
        class="org.springframework.transaction.
         interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
                <ref local="transactionManager2"/>
        </property>
        <property name="target">
                <ref local="auditManagerTarget"/>
        </property>
        <property name="transactionAttributes">
                <props>
                        <prop key="log">
                                PROPAGATION_REQUIRES_NEW
                        </prop>
                </props>
        </property>
</bean>
</beans>

 

  现在,为了演示,我们把注意力放到createOrderList和addLineItem这两个业务服务上。同时请注意,我们并没有要求最佳设计策略——你可能注意到了,addLineItem方法抛出了FacadeException异常,而createOrderList却没有。在生产设计中,您也许希望每一个服务方法都可以处理异常场景。

public class OrderListManagerImpl  implements OrderListManager{

private AuditManager auditManager;

public Long createOrderList
(OrderList orderList){
        Long orderId = orderListDAO.createOrderList(orderList);
       auditManager.log(new AuditObject(ORDER + orderId, CREATE));

        return orderId;
}

public void addLineItem
(Long orderId, LineItem lineItem)
        throws FacadeException{

        Long lineItemId = orderListDAO.addLineItem(orderId, lineItem);
       auditManager.log(new AuditObject(LINE_ITEM + lineItemId, CREATE));

        int numberOfLineItems = orderListDAO.
                queryNumberOfLineItems(orderId);
        if(numberOfLineItems > 2){
                log("Added LineItem " + lineItemId +" to Order " + orderId + ";
                        But rolling back *** !");
                throw new FacadeException("Make a new Order for this line item");
        }
        else{
                log("Added LineItem " + lineItemId +" to Order " + orderId + ".");
        }
}

//Other code goes here...

}

 

  为了创建一个异常场景来进行演示,我们引入了另一种业务规则,它规定一个特定的订单不能包含多于两个的线项目。现在应该注意,我们是从createOrderList和addLineItem中调用auditManager.log()方法的。您应该也注意到了为上述方法所指定的事务属性。

<bean id="orderListManager"
        class="org.springframework.transaction.
         interceptor.TransactionProxyFactoryBean">
        <property name="transactionAttributes">
                <props>
<prop key="createOrderList">
                                PROPAGATION_REQUIRED
                        </prop>
                        <prop key="addLineItem">
                                PROPAGATION_REQUIRED,-com.
                                example.exception.FacadeException
                        </prop>

                </props>
        </property>
</bean>

<bean id="auditManager" class="org.
        springframework.transaction.interceptor.
                TransactionProxyFactoryBean">
        <property name="transactionAttributes">
                <props>
<prop key="log">
                                PROPAGATION_REQUIRES_NEW
                        </prop>

                </props>
        </property>
</bean>

 

  PROPAGATION_REQUIRED等效于TX_REQUIRED,而PROPAGATION_REQUIRES_NEW等效于EJB中的TX_REQUIRES_NEW。如果我们想让服务方法始终在事务中运行,我们可以使用PROPAGATION_REQUIRED。当使用PROPAGATION_REQUIRED时,如果已经运行了一个TX,bean方法就会加入到该TX中;否则的话,Spring的轻量级TX管理器就会启动一个TX。如果在调用组件服务时我们总是希望开始新的事务,那么可以利用PROPAGATION_REQUIRES_NEW属性。

  我们还指定,当方法抛出FacadeException类型的异常时,addLineItem就总是回滚事务。这就达到了另一个粒度级别:在异常场景中,我们的控制可以精细到TX的具体结束方式。前缀符号“-”指定回滚TX,而前缀符号“+”指定提交TX。

  接下来的问题是,为什么我们要为log方法设置PROPAGATION_REQUIRES_NEW属性呢?这是由我们的以下需求决定的:无论主服务方法发生什么情况,对所有创建订单以及向系统添加线项目的尝试都要记录审计跟踪。也就是说,即使在createOrderList和addLineItem的实现过程中出现了异常也要记录审计跟踪。这仅在启动一个新的TX并在这个新的TX上下文中调用log的时候起作用。这就是为什么要为log设置PROPAGATION_REQUIRES_NEW TX属性的原因:如果对下述方法的调用成功了

auditManager.log(new AuditObject
(LINE_ITEM +lineItemId, CREATE));

  ,auditManager.log()就将在新的TX上下文中执行,而且只要auditManager.log()本身成功(即,没有抛出异常),新的上下文就会被提交。

  设置演示环境

  准备演示环境时,我参考了Spring Live这本书的流程:

  1. 下载并安装以下组件,这时请注意使用准确的版本,不然就会引起版本不兼容问题。
    • JDK 1_5_0_01或更高版本
    • Apache Tomcat 5.5.7
    • Apache Ant 1.6.2
    • Equinox 1.2
  2. 在系统中设置以下环境变量:
    • JAVA_HOME
    • CATALINA_HOME
    • ANT_HOME
  3. 把下列目录添加到您的PATH环境变量中,或者使用完全路径来执行脚本:
    • JAVA_HOMEin
    • CATALINA_HOMEin
    • ANT_HOMEin
  4. 要设置Tomcat,在文本编辑器中打开/conf/tomcat-users.xml文件,验证以下各行是否存在。如果不存在,必须手动添加进去:
<role rolename="manager"/>
 <user username="admin"
 password="admin" roles="manager"/> 
  1. 要创建基于Struts、Spring和Hibernate的Web应用程序,必须用Equinox来构建一个基本的框架程序(bare-bones starter application),它将包含预定义的文件夹结构、所有需要用到的.jar文件以及Ant构建脚本。把Equinox解压到一个文件夹中,它将创建一个equinox文件夹。将目录更改为equinox文件夹,输入命令ANT_HOMEinnt new -Dapp.name=myusers。这样就会创建一个与equinox同级的文件夹myusers。该文件夹的具体内容如下: 
    在Spring中配置Hibernate的事务(图四)

  图4 Equinox的myusers应用程序文件夹模板

  删除myuserswebWEB-INF文件夹下的所有。xml文件。

  复制equinoxxtrasstrutswebWEB-INFibstruts*.jar文件至myuserswebWEB-INFib文件夹下,这样,这个示例应用程序就可以利用struts了。

  从参考资料小节的示例代码中,解压myusersextra.zip到一个合适的位置。将目录更改为新创建的myusersextra文件夹,复制myusersextra文件夹中的所有内容,并将它们粘贴到myusers文件夹。

  打开命令提示符,将目录转至myusers目录下。执行CATALINA_HOMEinstartup.要从myusers文件夹启动Tomcat,这一点非常重要,否则数据库将不会创建在myusers文件夹中,从而导致在执行一些定义在build.xml中的任务时出现错误。

  再次打开命令提示符并将目录转至myusers目录下。执行ANT_HOMEinnt install.这将构建应用程序并把它部署到Tomcat中。这时,我们可以看到myusers中多了一个db目录,以便存放数据库appfuse1和appfuse2.

  打开浏览器并验证myusers应用程序已经部署在http://localhost:8080/myusers/上了。

  要重新安装应用程序,执行ANT_HOMEinnt remove,然后执行CATALINA_HOMEinshutdown关闭Tomcat.现在,从CATALINA_HOMEwebapps文件夹删除所有的myusers文件夹。然后执行CATALINA_HOMEinstartup重新启动Tomcat,并通过执行ANT_HOMEinnt install重新安装应用程序。

  运行演示

  为了运行测试用例,myusers estmxampleservice中提供了一个JUnit测试类,OrderListManagerTest.要执行它,可以在构建应用程序的命令提示符中输入以下命令:

CATALINA_HOMEinnt test -Dtestcase=OrderListManager  测试用例分为两个主要部分:第一部分创建一个由两个线项目组成的订单,然后把这两个线项目链接到订单中。它可以成功运行,如下所示:

OrderList orderList1 = new OrderList();
Long orderId1 = orderListManager.
        createOrderList(orderList1);
log("Created OrderList with id '"
        + orderId1 + "'...");
orderListManager.addLineItem(orderId1,lineItem1);
orderListManager.addLineItem(orderId1,lineItem2);

  第二部分执行类似的操作,但是这次我们试图向订单添加三个线项目,这将产生一个异常:
OrderList orderList2 = new OrderList();
Long orderId2 = orderListManager.
        createOrderList(orderList2);
log("Created OrderList with id '" + orderId2 + "'...");
orderListManager.addLineItem(orderId2,lineItem3);
orderListManager.addLineItem(orderId2,lineItem4);
//We know, we will have an exception here,still want to proceed
try{
  orderListManager.addLineItem
        (orderId2,lineItem5);
}
catch(FacadeException facadeException){
  log("ERROR : " + facadeException.getMessage());

}

  控制台的输出如图5所示:

在Spring中配置Hibernate的事务(图五)
图5 客户端控制台输出

  我们创建了Order1,并向其添加了两个ID为1和2的线项目。然后我们创建Order2,并尝试添加3个项目,前两个(ID为3和4)添加成功,但是图5显示,添加第三个项目(ID为5)时业务方法遇到了异常。因此,业务方法TX被回滚,数据库中没有ID为5的线项目。从控制台执行以下命令,就可以通过图6和图7进行验证:

CATALINA_HOMEinnt browse1

 

在Spring中配置Hibernate的事务(图六)

图6 appfuse1数据库中创建的订单

在Spring中配置Hibernate的事务(图七)

图7 appfuse1数据库中创建的线项目

  在接下来的也是最重要的演示部分中可以看出,订单和线项目保存在appfuse1数据库中,而审计对象保存在appfuse2数据库中。实际上,OrderListManager中的服务方法可以与多个数据库交互。启动appfuse2数据库,查看审计跟踪,如下所示:

CATALINA_HOMEinnt browse2

在Spring中配置Hibernate的事务(图八)

图8 创建到appfuse2数据库中的审计跟踪,包括失败TX的记录项

  表8最后一行尤其值得注意,RESOURCE列显示这一行对应的是LineItem5。但是当我们回过来看图7时,却发现并没有对应于LineItem5的线项目。哪里出错了呢?事实上并没有出错,图7没有的那一行其实正是这篇文章的关键所在,让我们来看看是怎么回事。

  我们知道,addLineItem()方法包含PROPAGATION_REQUIRED属性,而log()方法有PROPAGATION_REQUIRES_NEW属性。而且addLineItem()在内部调用了log()方法。因此,当我们试图向Order2添加第三个线项目时,就(按照我们的业务规则)引发了异常,于是这个线项目的创建以及将其链接到Order2的操作都被回滚了。但是,因为还从addLineItem()中调用了log(),还因为log()具有PROPAGATION_REQUIRES_NEW TX属性,回滚addLineItem()将不会造成回滚log(),因为log()是在一个新的TX中执行。

  让我们对log()的TX属性做一下改动,把PROPAGATION_REQUIRES_NEW替换为PROPAGATION_SUPPORTS。PROPAGATION_SUPPORTS属性允许服务方法在客户端有TX上下文时在客户端TX中运行;如果客户端没有TX,就不用TX而直接运行。您可能必须重新安装应用程序,以便数据库中已经可用的数据可以自动刷新。重新安装请按照“设置演示环境”中的步骤12进行。

  如果再次运行,我们会发现一点不同。这次,在试图向Order2添加第三个线项目时依然有异常,这将回滚试图添加第三个线项目的事务。而这个方法又调用了log()方法。但是由于它的PROPAGATION_SUPPORTS TX属性,log()将在与addLineItem()方法相同的TX上下文中调用。由于addLineItem()回滚,log()也回滚了,没有留下回滚的TX的审计跟踪。所以在图9中没有对应于失败TX的审计跟踪记录项!

在Spring中配置Hibernate的事务(图九)

图9 创建在appfuse2数据库中的审计跟踪,没有失败TX的记录项

  我们改动的仅仅是Spring配置文件中的TX属性,就产生了这样不同的事务行为,如下所示:

<bean id="auditManager" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionAttributes">
        <props>
<!-- prop key="log">
                PROPAGATION_REQUIRES_NEW
            </prop -->
            <prop key="log">
                PROPAGATION_SUPPORTS
            </prop>

        </props>
    </property>
</bean>

 

  这就是声明式事务管理的效果。自从EJB出现以来我们就一直在使用这种方法,但是我们需要一个高端的应用服务器来驻留EJB组件。现在,我们可以看到,利用Spring,没有EJB服务器也可以达到类似的结果。

  结束语

  这篇文章重点介绍了J2EE领域的强大组合之一:Spring和Hibernate。利用二者的功能,现在对于容器管理持久性(Container-Managed Persistence,CMP)、容器管理关系(Container-Managed Relationships,CMR)和声明式事务管理,我们多了一种技术选择。虽然Spring不能视为EJB的替代方案,但是它提供的许多功能,例如普通Java对象的声明式事务管理,使得在许多项目中没有EJB也完全可以。

  本文的目的不是要寻找EJB的替代方案,而是为当前的问题找出一个最可行的技术方案。至于Spring和Hibernate的轻量级组合的更多功能,就留给我们的读者去探索了。

分享到:
评论

相关推荐

    Spring事务流程图

    Spring事务管理是Spring框架的核心特性之一,主要用于处理应用程序中的数据一致性问题。在Spring中,事务管理分为编程式和声明式两种方式。本篇文章将详细解释Spring事务管理的流程,以及如何通过时序图来理解这一...

    Spring事务管理Demo

    Spring事务管理的目的是确保数据的一致性和完整性,尤其是在多操作、多资源的环境中。本Demo将深入探讨Spring如何实现事务的管理。 首先,Spring提供了两种主要的事务管理方式:编程式事务管理和声明式事务管理。 ...

    spring事务与数据库操作

    ### Spring事务与数据库操作 #### 一、Spring的声明式事务管理 在现代软件开发中,事务处理是非常关键的一部分,特别是在涉及多个数据操作时。Spring框架提供了强大的事务管理能力,可以方便地集成到应用程序中。...

    Spring事务原理、Spring事务配置的五种方式

    Spring事务原理和配置 Spring事务原理是指Spring框架中的一种机制,用于管理事务,并提供了多种配置方式。事务是指一系列的操作,作为一个整体执行,如果其中某个操作失败,整个事务将回滚。Spring事务原理围绕着两...

    Spring事务管理开发必备jar包

    本资源包提供了进行Spring事务管理开发所需的所有关键库,包括框架基础、核心组件、AOP(面向切面编程)支持、日志处理、编译工具以及与数据库交互的相关jar包。下面将对这些知识点进行详细解释: 1. **Spring框架*...

    Spring事务管理的jar包

    这个版本的Spring事务管理支持JDBC、Hibernate、JPA、iBatis等多种数据访问技术,可以无缝集成到各种持久层框架中。 在Spring中,事务管理器(如DataSourceTransactionManager或HibernateTransactionManager)是...

    Spring事务小demo

    同时,数据库连接池如Druid、HikariCP等也需要配置,它们提供了高效的连接管理和事务支持。 5. **数据库配置**:在Spring的applicationContext.xml或对应的配置类中,需要配置DataSource和JdbcTemplate或Mybatis的...

    spring事务案例分析.zip

    本主题将深入探讨“Spring事务案例分析.zip”中的关键知识点,包括Spring事务管理及其在实际项目中的应用。 首先,我们来了解什么是Spring事务管理。在分布式系统或数据库操作中,事务管理是确保数据一致性和完整性...

    Spring对事务支持

    这份文档以例子的形式讲诉了Spring对事务支持的知识,希望可以帮助学习的人!

    实验 spring 声明事务

    在实际应用中,Spring 的声明式事务管理不仅限于JDBC,还支持其他数据访问技术,如Hibernate、MyBatis等。同时,Spring还提供了编程式事务管理,允许开发者在代码中手动管理事务,但这种方式通常在更复杂的场景或...

    spring事物 支持

    接下来,我们将深入探讨Spring事务支持的相关知识点。 首先,Spring事务管理分为编程式事务管理和声明式事务管理两种方式。 1. **编程式事务管理**:这是通过编程的方式来控制事务的边界。开发者可以使用`...

    Spring事务传播机制.docx

    当我们在使用 Spring 所提供的事务功能时,如果是仅仅处理单个的事务,是比较容易把握事务的提交与回滚,不过一旦引入嵌套事务后,多个事务的回滚和提交就会变得复杂起来,各个事务之间是如何相互影响的,是一个值得...

    Spring事务传播Demo.zip

    本篇将基于"Spring事务传播Demo"来深入探讨Spring事务管理和传播行为。 首先,我们需要理解什么是事务。在数据库操作中,事务是一组操作,这些操作要么全部执行,要么全部不执行,以确保数据的一致性和完整性。在...

    spring事务详解

    Spring事务详解 Spring框架的事务管理功能是Java企业级开发中的重要组成部分,它将事务管理从具体的业务逻辑和数据访问逻辑中独立出来,实现了关注点分离。这种分离不仅降低了事务管理的复杂性,而且增强了代码的可...

    Spring事务与Java事务比较

    Spring 提供了 TransactionTemplate 和 PlatformTransactionManager 接口来支持编程式事务管理。 2. 声明式事务管理:这是 Spring 的一大特色,它允许开发者在配置文件或注解中声明事务的边界,而不必在业务代码中...

    spring 事务基于注解模式

    Spring事务管理分为编程式和声明式两种。编程式事务管理通过编程的方式(如使用`TransactionTemplate`或直接调用`PlatformTransactionManager`)来控制事务的开始、提交、回滚等操作。而声明式事务管理则是在配置...

    Spring事务处理-ThreadLocal的使用

    例如,`HikariCP`和`C3P0`等流行的连接池实现都支持与Spring事务管理的无缝集成。 在实际应用中,理解ThreadLocal在Spring事务处理中的作用有助于优化并发性能和解决多线程环境下的事务问题。例如,如果线程之间...

    spring 事务管理的理解

    Spring 框架是Java开发中...理解并熟练掌握Spring事务管理,对于提升应用程序的稳定性和可靠性至关重要。在实际开发中,结合声明式事务管理、事务传播行为、隔离级别和回滚规则,可以有效地确保数据的完整性和一致性。

Global site tag (gtag.js) - Google Analytics