歡迎光臨
每天分享高質量文章

Transaction 在 Controller 層的探索

(點選上方公眾號,可快速關註)


來源:一年一度的夏天 ,

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

一般開發中事務要求我們放在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 getCity(@RequestParam(“id”) Integer id) {

      City one = cityService.getOne(id);

      BaseResult 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 getCity(@RequestParam(“id”) Integer id) {

      City one = cityService.getOne(id);

      BaseResult 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 系結的時候,這個類不是一個代理,所以才會匹配成功。

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂