事物在Controller层的探索

Transaction在Controller层的探索

一般开发中事务要求我们放在Service层,可是有些情况,我们可能会要求放在Controller层,你有没有碰到过这样的需求呢?那么放到Controller层事务会生效吗?会产生什么问题呢?下面一起来看看

I、透过现象看本质

第一种情况

Controller层代码如下

@RestController 
@RequestMapping("/city") 
public class CityControllerImpl implements CityController {
    @Autowired   
    private CityService cityService;
        
    @Override   
    @RequestMapping(value = "getCity",method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_UTF8_VALUE)     
    @Transcational   
    public BaseResult<City> getCity(@RequestParam("id") Integer id) {       
  ‍      City one = cityService.getOne(id);       
        BaseResult<City> baseResult=new BaseResult<>();       
        baseResult.setData(one);       
        return baseResult; ‍  
    } 
}

运行结果

对的,你没有看错,当Transactional加载Controller层时出现404异常

第二种情况

Controller层代码如下

@RestController 
@RequestMapping("/city") 
public class CityControllerImpl  {
   @Autowired   private CityService cityService;    
   @RequestMapping(value = "getCity",method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_UTF8_VALUE)   
   @Transactional   
   public BaseResult<City> getCity(@RequestParam("id") Integer id) {       
       City one = cityService.getOne(id);       
       BaseResult<City> baseResult=new BaseResult<>();       
       baseResult.setData(one);       
       return baseResult;   
   } 
}

跟上面的区别,就是没有实现CityController接口了,那么我们运行一下,会有什么结果呢?

运行结果如下:

{ data: null, message: null, status: 0 }

第二种情况居然没有啥问题,那么Transactional是否正常回滚呢?这里答案我直接告诉大家了,即使是换成有数据更改的接口,我们的事务是生效的。

第三种情况

笔者测试使用支持==JAX-RS 2.0==的 Resteasy 测试,发现是没有这个问题的,大家可以自测一下Jersey是不是存在这个问题,推断应该没有

II、熟悉本质解现象

1. 区别

可以看出,我们两个Controller的区别就是一个有实现接口,一个没有实现,为什么差别会这么大呢?

2. 事务的本质

我们知道事务是基于代理实现的,目前Spring中有JDK动态代理和CGLIB代理两种代理,那么跟Spring选择的代理有没有关系呢?我们看一下Spring在代理类的时候选择使用何种代理的源代码。如下:

@Override     
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {        
     if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {             
         Class<?> targetClass = config.getTargetClass();             
         if (targetClass == null) {                 
             throw new AopConfigException("TargetSource cannot determine target class: " +                         
                    "Either an interface or a target is required for proxy creation.");             
         }             
         if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {                 
             return new JdkDynamicAopProxy(config);             
         }             
         return new ObjenesisCglibAopProxy(config);         
     } else {             
         return new JdkDynamicAopProxy(config);         
     }     
 }

这是Spring创建代理比较核心的一段代码,在类 DefaultAopProxyFactory 中,不管加没有加接口,Spring看到了@Transactional注解都会给我们的Controller注册为一个代理对象。注意:Spring并非对所有的Controller都会创建代理类,假如我们的Controller没有暴露任何切面,Spring并不会创建一个代理类,这里可能大家会感到奇怪,我们这里打个TAG,文末讲解。

继续刚刚的话题,第一种情况,由于我们的Controller有接口,所以就走了JDK代理,相反第二种走了Cglib代理。OK, 我们的CityControllerImpl现在是一个代理类。那么为什么会发生404异常呢?

3. SpringMvc的原理

为什么Controller变成代理之后,就会404异常了,肯定跟我们的SpringMVC有关,我们看一下SpringMVC的核心类 AbstractHandlerMethodMapping 这个类可以绑定URL和需要执行处理器的哪个方法。这个抽象类实现了initializingBean接口,其实主要的注册URL操作则是通过这个接口的afterPropertiesSet()接口方法来调用的。然后调用initHandlerMethods 方法进行绑定URL。方法详细如下:

protected void initHandlerMethods() {
         if (logger.isDebugEnabled()) {             
             logger.debug("Looking for request mappings in application context: " + getApplicationContext());         
         }         
         String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?                 
             BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :                 
             getApplicationContext().getBeanNamesForType(Object.class));          
         for (String beanName : beanNames) {             
             if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {                 
                 Class<?> beanType = null;                 
             try {                     
                 beanType = getApplicationContext().getType(beanName);                 
             } catch (Throwable ex) {                     
                 // An unresolvable bean type, probably from a lazy bean - let's ignore it.                     
                if (logger.isDebugEnabled()) {                         
                    logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);                     
                }                 
            }                 
                if (beanType != null && isHandler(beanType)) {                     
                detectHandlerMethods(beanName);                 
                }             
            }         
        }         
        handlerMethodsInitialized(getHandlerMethods());     
}

beanType中取出来是 CityControllerImpl 代理类,这里大家注意,代码第21行,有一个isHandler方法,这个方法用于判定这个类是不是Handler,其中代码如下:

@Override     
protected boolean isHandler(Class<?> beanType) {
      return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||                 
          AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));     
}

看到这里相信大家已经很明白了,这里就是看你这个类上面有没有Controller注解和RequestMapping注解。如果有,就建立相关的映射关系(URL->Handler)

其中有接口的是被JDK代理的,生成的是JDK代理类

JDK的动态代理是靠多态和反射来实现的,它生成的代理类需要实现你传入的接口,并通过反射来得到接口的方法对象,并将此方法对象传参给增强类的invoke方法去执行,从而实现了代理功能。

CityController生成的代理类文件如下:

public final class cityControllerImpl extends Proxy implements Proxy86 {  
  private static Method m1;  
  private static Method m32;  
  private static Method m7;  

  public cityControllerImpl(InvocationHandler var1) throws  {  
      super(var1);  
  }  

  public final TargetSource getTargetSource() throws  {  
      try {  
          return (TargetSource)super.h.invoke(this, m8, (Object[])null);  
      } catch (RuntimeException | Error var2) {  
          throw var2;  
      } catch (Throwable var3) {  
          throw new UndeclaredThrowableException(var3);  
      }  
  }  

  public final void addAdvice(int var1, Advice var2) throws AopConfigException {  
      try {  
          super.h.invoke(this, m21, new Object[]{var1, var2});  
      } catch (RuntimeException | Error var4) {  
          throw var4;  
      } catch (Throwable var5) {  
          throw new UndeclaredThrowableException(var5);  
      }  
  }  

  public final BaseResult getCity(Integer var1) throws  {  
      try {  
          return (BaseResult)super.h.invoke(this, m27, new Object[]{var1});  
      } catch (RuntimeException | Error var3) {  
          throw var3;  
      } catch (Throwable var4) {  
          throw new UndeclaredThrowableException(var4);  
      }  
  }  

}

类已经被精简过,我们看到生成的代理类中完全没有@Controller @RequestMapping 注解,所以isHandler方法执行失败,所以根本不会加到SpringMvc的控制器处理方法中去,当URL请求过来的时候,找不到对应的处理器处理,所以就报404错误啦

没有接口的是被CGLIB代理的,生成的是CGlib代理类

CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础

public class CityControllerImpl$$EnhancerBySpringCGLIB$$8cae5808 extends CityControllerImpl implements SpringProxy, Advised, Factory {
  private boolean CGLIB$BOUND;  public static Object CGLIB$FACTORY_DATA;  
  private static final ThreadLocal CGLIB$THREAD_CALLBACKS;  
  private static final Callback[] CGLIB$STATIC_CALLBACKS;  
  final BaseResult CGLIB$getCity$0(Integer var1) {      
      return super.getCity(var1);
  }  
  public final BaseResult getCity(Integer var1) {
      MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;      
      if (this.CGLIB$CALLBACK_0 == null) {
          CGLIB$BIND_CALLBACKS(this);
          var10000 = this.CGLIB$CALLBACK_0;
      }      
      return var10000 != null ? (BaseResult)var10000.intercept(this, CGLIB$getCity$0$Method, new Object[]{var1}, CGLIB$getCity$0$Proxy) : super.getCity(var1);
  }
}

==其实isHandler方法会代理类的接口和父类进行扫描==,看你有没有这个注解,JDK代理中cityControllerImpl接口和父类都没有注解,而CGlib代理的父类是CityControllerImpl 这个原始的类, 所以返回为真

4. 思考

如果Controller层不加@Transcational注解的时候,为什么又不会产生404异常呢?其实如果你Controller不加任何织入代码的话(自定义aop切面等,有兴趣的可以用AspectJ试一下织入Controller层的Method方法会发生什么事情),Spring是不会给你的类生成代理的,也就是在AbstractHandlerMethodMapping 绑定的时候,这个类不是一个代理,所以才会匹配成功。

原文地址:

https://blog.csdn.net/u011410529/article/details/79671309

原文发布于微信公众号 - java技术学习之道(javajsxxzd)

原文发表时间:2018-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术博客

C#反射

        Reflection,中文翻译为反射。         这是.Net中获取运行时类型信息的方式,.Net的应用程序由几个部分:‘程序集(Asse...

832
来自专栏浪淘沙

Java加载项目中properties配置文件的三种方式

794
来自专栏小勇DW3

Mybatis使用动态代理实现拦截器功能

  拦截器顾名思义为拦截某个功能的一个武器,在众多框架中均有“拦截器”。这个Plugin有什么用呢?或者说拦截器有什么用呢?可以想想拦截器是怎么实现的。Plug...

782
来自专栏一个会写诗的程序员的博客

《Kotin 极简教程》第10章 Kotlin与Java互操作

在前面的章节中,我们已经学习了Kotlin的基础语法、类型系统、泛型与集合类、面向对象与函数式编程等主题,在上一章中我们还看到了Kotlin提供的轻量级并发编程...

782
来自专栏老码农专栏

原 荐 OSGL 工具库 - 类型转换的艺术

1433
来自专栏函数式编程语言及工具

泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型。我们知道,在所有编程语言中,数据类型是支持软件编程的基础。同样,泛函数据类型Foldable,Mon...

1927
来自专栏程序员的SOD蜜

实体类的枚举属性--原来支持枚举类型这么简单,没有EF5.0也可以

    通常,我们都是在业务层和界面层使用枚举类型,这能够为我们编程带来便利,但在数据访问层,不使用枚举类型,因为很多数据库都不支持,比如我们现在用的SqlSe...

20310
来自专栏后端沉思录

自定义参数解析器

开发中,app端给服务端会传基础参数、其他参数,一般基础参数app端都会传给服务端,其他参数则是根据不同接口传不同参数。若以表单的形式提交的数据:

1093
来自专栏java达人

Spring aop 的代理机制

Spring aop 是通过代理实现的,代理有静态代理,jdk动态代理和cglib动态代理,代理就像我们生活中的房产中介,你不直接与房主,银行接触,而是通过中介...

1929
来自专栏CSDN技术头条

Spring 框架之 AOP 原理剖析

前言 AOP(Aspect Oriented Programming)面向切面编程是 Spring 框架最核心的组件之一,它通过对程序结构的另一种考虑,补充了...

6094

扫码关注云+社区