Desarrollo con Spring Framework: Anotaciones, AOP, MyBatis y Transacciones

Para habilitar el uso de anotaciones en Spring, es necesario configurar el escaneo de componentes mediante XML. Esto permite que el contenedor detecte y gestione automáticamente las clases anotadas dentro de los paquetes especificados.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.app.domain"/>

</beans>

Entre las anotaciones más utilizadas se encuentran:

  • @Component: Registra el bean en el contenedor. Existen variantes especializadas como @Controller, @Service y @Repository.
  • @Scope: Define el ámbito del bean (singleton, prototype, etc.).
  • @Value: Inyecta valores literales en propiedades o métodos.
  • @Autowired, @Qualifier, @Resource: Realizan el cableado automático de dependencias.
@Component
@Scope(scopeName = "singleton")
public class Employee {

    @Value("alice")
    private String employeeName;

    @Value("charlie")
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }

    public String getEmployeeName() {
        return employeeName;
    }
}

Configuración del contenedor mediante código Java

Spring permite prescindir del XML y configurar el contenedor íntegramente desde clases Java. Las anotaciones clave son:

  • @Configuration: Marca la clase como configuración.
  • @Bean: Registra el objeto retornado como un bean.
  • @Import: Combina múltiples clases de configuración.
  • @ComponentScan: Define los paquetes a escanear.
@Configuration
@Import(AspectConfiguration.class)
public class AppConfig {

    @Bean(name = "employee")
    public Employee createEmployee() {
        return new Employee();
    }
}

Para arrancar el contador desde esta configuración, se utiliza AnnotationConfigApplicationContext:

public class AppTest {

    @Test
    public void runTest() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        Employee emp = ctx.getBean("employee", Employee.class);
        System.out.println(emp.getEmployeeName());
    }
}

Patrón de proxy estático

El proxy estático consiste en crear una clase intermedia que envuelve al objeto real y añade lógica adicional antes o después de la ejecución del método original.

public interface Task {
    void execute();
}

public class TaskImpl implements Task {
    @Override
    public void execute() {
        System.out.println("ejecutando tarea");
    }
}

public class TaskProxy implements Task {

    private Task target;

    public TaskProxy(Task target) {
        this.target = target;
    }

    @Override
    public void execute() {
        System.out.println("--- inicio ---");
        target.execute();
        System.out.println("--- fin ---");
    }
}

public class Runner {
    public static void main(String[] args) {
        TaskImpl realTask = new TaskImpl();
        TaskProxy proxy = new TaskProxy(realTask);
        proxy.execute();
    }
}

El principal inconveniente es que, al añadir nuevos métodos a la interfaz, tanto la clase real como el proxy deben modificarse, duplicando el código.

Patrón de proxy dinámico

El proxy dinámico genera la clase proxy en tiempo de ejecución, evitando la creación manual de clases proxy para cada interfaz.

public interface Task {
    void execute();
}

public class TaskImpl implements Task {
    @Override
    public void execute() {
        System.out.println("ejecutando tarea");
    }
}

public class DynamicTaskHandler implements InvocationHandler {

    private Task target;

    public DynamicTaskHandler(Task target) {
        this.target = target;
    }

    public Task createProxy() {
        return (Task) Proxy.newProxyInstance(
                getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("registrando log");
        method.invoke(target, args);
        return proxy;
    }
}

public class Runner {
    public static void main(String[] args) {
        TaskImpl realTask = new TaskImpl();
        DynamicTaskHandler handler = new DynamicTaskHandler(realTask);
        Task proxyTask = handler.createProxy();
        proxyTask.execute();
    }
}

La ventaja es que el handler DynamicTaskHandler no necesita modificarse aunque el objeto real incorpore nuevos métodos. Existen dos enfoques principales: el proxy dinámico de JDK (basado en interfaces) y CGLIB (basado en herencia de clases).

Spring AOP

Spring AOP permite separar preocupaciones transversales como logging, seguridad o transacciones del código de negocio. Se requiere el siguiente conjunto de dependencias:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.8</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Implementación con API de Spring

Se definen las clases de negocio y los advices implementando las interfaces de Spring AOP:

public interface AccountService {
    void create();
}

public class AccountServiceImpl implements AccountService {
    @Override
    public void create() {
        System.out.println("creando cuenta");
    }
}

public class PostAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("ejecución completada");
    }
}

public class PreAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("iniciando ejecución");
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="accountService" class="com.app.AccountServiceImpl"/>
    <bean id="preAdvice" class="com.app.PreAdvice"/>
    <bean id="postAdvice" class="com.app.PostAdvice"/>

    <aop:config>
        <aop:pointcut id="servicePointcut" expression="execution(* com.app.*.*(..))"/>
        <aop:advisor advice-ref="preAdvice" pointcut-ref="servicePointcut"/>
        <aop:advisor advice-ref="postAdvice" pointcut-ref="servicePointcut"/>
    </aop:config>
</beans>

Implementación mediante XML con aspecto propio

public class AuditLog {

    public void logBefore() {
        System.out.println("--- previo ---");
    }

    public void logAfter() {
        System.out.println("--- posterior ---");
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="accountService" class="com.app.AccountServiceImpl"/>
    <bean id="auditLog" class="com.app.AuditLog"/>

    <aop:config>
        <aop:aspect id="auditAspect" ref="auditLog">
            <aop:pointcut id="servicePointcut" expression="execution(* com.app.*.*(..))"/>
            <aop:before method="logBefore" pointcut-ref="servicePointcut"/>
            <aop:after method="logAfter" pointcut-ref="servicePointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

Implementación mediante anotaciones

@Aspect
public class AuditAspect {

    @Pointcut("execution(* com.app.*.*(..))")
    public void auditPointcut() {}

    @Before("auditPointcut()")
    public void logBefore() {
        System.out.println("antes de la ejecución");
    }

    @Around("auditPointcut()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("inicio envolvente");
        pjp.proceed();
        System.out.println(pjp.getSignature().getDeclaringType().getName() + "." + pjp.getSignature().getName());
        System.out.println("fin envolvente");
        return null;
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="accountService" class="com.app.AccountServiceImpl"/>
    <bean id="auditAspect" class="com.app.AuditAspect"/>
    <aop:aspectj-autoproxy/>
</beans>

Selección del mecanismo de proxy

El atributo proxy-target-class controla el tipo de proxy utilizado. Con false (valor por defecto) se emplea el proxy dinámico de JDK basado en interfaces. Con true se utiliza CGLIB, que genera subclases.

<aop:aspectj-autoproxy proxy-target-class="false"/>

Integración de Spring con MyBatis mediante SqlSessionTemplate

Para esta integración se requieren las siguientes dependencias:

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.8</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.8</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.6</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>
</dependencies>

El archivo de configuración de MyBatis se mantiene mínimo:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <typeAliases>
        <package name="com.app.dao"/>
    </typeAliases>
</configuration>

La entidad de dominio:

@Data
public class Customer {
    private Integer id;
    private String name;
    private Integer age;
}

El mapper correspondiente:

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.app.dao.CustomerDao">
    <select id="findAll" resultType="Customer">
        select * from tb_customer
    </select>
</mapper>

La configuración de Spring define el origen de datos, la fábrica de sesiones y el SqlSessionTemplate:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/springboot"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath*:com/app/dao/*.xml"/>
    </bean>

    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"/>
    </bean>

    <bean id="customerDao" class="com.app.dao.impl.CustomerDaoImpl">
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"/>
    </bean>
</beans>

La interfaz del DAO y su implementación que utiliza SqlSessionTemplate para obtener el mapper:

public interface CustomerDao {
    List<Customer> findAll();
}

public class CustomerDaoImpl implements CustomerDao {

    private SqlSessionTemplate sqlSessionTemplate;

    public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
    }

    @Override
    public List<Customer> findAll() {
        CustomerDao mapper = sqlSessionTemplate.getMapper(CustomerDao.class);
        return mapper.findAll();
    }
}

public class CustomerTest {

    @Test
    public void runTest() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        CustomerDao dao = ctx.getBean("customerDao", CustomerDao.class);
        List<Customer> customers = dao.findAll();
        customers.forEach(System.out::println);
    }
}

Integración de Spring con MyBatis mediante SqlSessionDaoSupport

Una alternativa consiste en heredar de SqlSessionDaoSupport, que proporciona acceso al SqlSession directamente:

public class CustomerDaoImpl2 extends SqlSessionDaoSupport implements CustomerDao {

    @Override
    public List<Customer> findAll() {
        CustomerDao mapper = getSqlSession().getMapper(CustomerDao.class);
        return mapper.findAll();
    }
}

En este enfoque, la implementación del DAO debe recibir la inyección de sqlSessionFactory:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/springboot"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath*:com/app/dao/*.xml"/>
    </bean>

    <bean id="customerDao2" class="com.app.dao.impl.CustomerDaoImpl2">
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
</beans>

Configuración de transacciones mediante XML

Para gestionar transacciones de forma declarativa, se define un gestor de transacciones y se asocian los advices correspondientes a través de AOP:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/springboot"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath*:com/app/dao/*.xml"/>
    </bean>

    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"/>
    </bean>

    <bean id="customerDao" class="com.app.dao.impl.CustomerDaoImpl">
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"/>
    </bean>

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <aop:config>
        <aop:pointcut id="daoPointcut" expression="execution(* com.app.dao.impl.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="daoPointcut"/>
    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="insert" propagation="REQUIRED"/>
            <tx:method name="remove" propagation="REQUIRED"/>
            <tx:method name="modify" propagation="REQUIRED"/>
            <tx:method name="find" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
</beans>

Etiquetas: Spring Framework Spring AOP MyBatis Anotaciones Spring Transacciones Spring

Publicado el 9-25 08:26