深入解析spring?AOP原理及源碼
@EnableAspectJAutoProxy
@EnableAspectJAutoProxy注解用于開啟AOP功能,那么這個(gè)注解底層到底做了什么呢?
查看@EnableAspectJAutoProxy的源碼,發(fā)現(xiàn)它使用@Import注解向Spring容器中注入了一個(gè)類型為AspectJAutoProxyRegistrar的Bean:
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 注入一個(gè)bean名字為org.springframework.aop.config.internalAutoProxyCreator的AspectJAwareAdvisorAutoProxyCreator
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
// proxyTargetClass為true
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
// exposeProxy為true
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}
}AspectJAutoProxyRegistrar實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口,而ImportBeanDefinitionRegistrar是spring提供的擴(kuò)展點(diǎn)之一,主要用來向容器中注入BeanDefinition,spring會(huì)根據(jù)BeanDefinion來生成Bean。
那么AspectJAutoProxyRegistrar到底向容器中注入了什么BeanDefinion呢?
org.springframework.aop.config.AopConfigUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary(org.springframework.beans.factory.support.BeanDefinitionRegistry)
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null);
}
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {
// AnnotationAwareAspectJAutoProxyCreator
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
}
}
return null;
}
// 注入AspectJAwareAdvisorAutoProxyCreator
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}從源碼可以發(fā)現(xiàn)AspectJAutoProxyRegistrar向容器中注入了一個(gè)類型為AnnotationAwareAspectJAutoProxyCreator的Bean。
那么AnnotationAwareAspectJAutoProxyCreator又是干什么的呢?
AnnotationAwareAspectJAutoProxyCreator主要實(shí)現(xiàn)了三個(gè)接口(由父類AbstractAutoProxyCreator實(shí)現(xiàn)):
- 實(shí)現(xiàn)了BeanFactoryAware,內(nèi)部持有BeanFactory的引用。
- 實(shí)現(xiàn)了SmartInstantiationAwareBeanPostProcessor(InstantiationAwareBeanPostProcessor).postProcessBeforeInstantiation,這個(gè)方法在bean的實(shí)例化(bean創(chuàng)建之前)之前執(zhí)行。
- 實(shí)現(xiàn)了BeanPostProcessor.postProcessBeforeInitialization(),這個(gè)方法在bean的初始化之前(bean創(chuàng)建之后,屬性被賦值之前)執(zhí)行,BeanPostProcessor.postProcessAfterInitialization()在bean的初始化之后執(zhí)行。
AnnotationAwareAspectJAutoProxyCreator的繼承結(jié)構(gòu):

找切面
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
/**
* @see AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors()
*/
// 獲取容器中所有的切面Advisor
// 這里返回的切面中的方法已經(jīng)是有序的了,先按注解順序(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),再按方法名稱
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// 獲取所有能夠作用于當(dāng)前Bean上的Advisor
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
/**
* @see AspectJAwareAdvisorAutoProxyCreator#extendAdvisors(java.util.List)
*/
// 往集合第一個(gè)位置加入了一個(gè)DefaultPointcutAdvisor
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
/**
* @see AspectJAwareAdvisorAutoProxyCreator#sortAdvisors(java.util.List)
*/
// 這里是對切面進(jìn)行排序,例如有@Order注解或者實(shí)現(xiàn)了Ordered接口
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
protected List<Advisor> findCandidateAdvisors() {
// Add all the Spring advisors found according to superclass rules.
// 獲取容器中所有的切面Advisor
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
// 這里還需要解析@Aspect注解,生成Advisor
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}org.springframework.aop.support.AopUtils#findAdvisorsThatCanApply
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new ArrayList<>();
// InstantiationModelAwarePointcutAdvisorImpl
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
// IntroductionAdvisor類型為引入切面,具體類型為DeclareParentsAdvisor
eligibleAdvisors.add(candidate);
}
}
boolean hasIntroductions = !eligibleAdvisors.isEmpty();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor) {
// already processed
continue;
}
// PointCut中的ClassFilter.match 匹配類
// PointCut中的MethodMatcher.match 匹配方法
if (canApply(candidate, clazz, hasIntroductions)) {
// @Aspect,類型為InstantiationModelAwarePointcutAdvisorImpl
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}代理對象的創(chuàng)建
代理對象的創(chuàng)建時(shí)機(jī)位于bean的初始化之后,因?yàn)榇韺ο髢?nèi)部還是需要去調(diào)用目標(biāo)對象的方法,所以需要讓目標(biāo)對象實(shí)例化并完成初始化后才會(huì)創(chuàng)建代理對象。
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
// 先從緩存中獲取代理對象
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 按需生成代理對象
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
/**
* @see AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean(java.lang.Class, java.lang.String, org.springframework.aop.TargetSource)
*/
// 獲取與當(dāng)前Bean匹配的切面
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 創(chuàng)建代理
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
// 緩存
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}
// 創(chuàng)建代理工廠
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if (!proxyFactory.isProxyTargetClass()) {
// 進(jìn)來說明proxyTargetClass=false,指定JDK代理
if (shouldProxyTargetClass(beanClass, beanName)) {
// 進(jìn)來這里說明BD中有個(gè)屬性preserveTargetClass=true,可以BD中屬性設(shè)置的優(yōu)先級最高
proxyFactory.setProxyTargetClass(true);
}
else {
// 這里會(huì)判斷bean有沒有實(shí)現(xiàn)接口,沒有就只能使用CGlib
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors); // 切面
proxyFactory.setTargetSource(targetSource); // 目標(biāo)對象
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// 使用JDK或者CGlib創(chuàng)建代理對象
return proxyFactory.getProxy(getProxyClassLoader());
}org.springframework.aop.framework.JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}這里主要看JDK動(dòng)態(tài)代理的實(shí)現(xiàn),Proxy.newProxyInstance()的第三個(gè)參數(shù)為InvocationHandler,而這里傳的是this,也就是當(dāng)前的類肯定實(shí)現(xiàn)了InvocationHandler接口。
代理方法的執(zhí)行
由于是JDK動(dòng)態(tài)代理,那么代理方法的調(diào)用肯定會(huì)進(jìn)入InvocationHandler.invoke()方法中,這里的InvocationHandler的實(shí)現(xiàn)類為org.springframework.aop.framework.JdkDynamicAopProxy。
org.springframework.aop.framework.JdkDynamicAopProxy#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget(); // 目標(biāo)對象
Class<?> targetClass = (target != null ? target.getClass() : null); // 目標(biāo)對象的類型
// Get the interception chain for this method.
// 這里會(huì)對方法進(jìn)行匹配,因?yàn)椴皇悄繕?biāo)對象中的所有方法都需要增強(qiáng)
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
// 沒有匹配的切面,直接通過反射調(diào)用目標(biāo)對象的目標(biāo)方法
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
/**
* @see ReflectiveMethodInvocation#proceed()
*/
// 這里才是增強(qiáng)的調(diào)用,重點(diǎn),火炬的傳遞
retVal = invocation.proceed();
}
// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
org.springframework.aop.framework.ReflectiveMethodInvocation#proceed
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 執(zhí)行到最后一個(gè)Advice,才會(huì)到這里執(zhí)行目標(biāo)方法
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
// dm.isRuntime()=true的走這
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
// 走這
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
interceptorsAndDynamicMethodMatchers中第一個(gè)advice為org.springframework.aop.interceptor.ExposeInvocationInterceptor。
ExposeInvocationInterceptor#invoke
org.springframework.aop.interceptor.ExposeInvocationInterceptor#invoke
private static final ThreadLocal<MethodInvocation> invocation =
new NamedThreadLocal<>("Current AOP method invocation");
public Object invoke(MethodInvocation mi) throws Throwable {
MethodInvocation oldInvocation = invocation.get();
invocation.set(mi);
try {
return mi.proceed();
}
finally {
invocation.set(oldInvocation);
}
}
ExposeInvocationInterceptor#invoke,只干了一件事就是將MethodInvocation加入到了ThreadLocal中,這樣后續(xù)可以在其他地方使用ExposeInvocationInterceptor#currentInvocation獲取到MethodInvocation,而MethodInvocation中封裝了目標(biāo)對象,目標(biāo)方法,方法參數(shù)等信息。
環(huán)繞通知的執(zhí)行
org.springframework.aop.aspectj.AspectJAroundAdvice#invoke
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
return invokeAdviceMethod(pjp, jpm, null, null);
}
這里會(huì)去調(diào)用環(huán)繞通知的增強(qiáng)方法,而環(huán)繞通知的增強(qiáng)方法中會(huì)執(zhí)行proceedingJoinPoint.proceed(),這樣就會(huì)調(diào)用下一個(gè)MethodInterceptor–>MethodBeforeAdviceInterceptor。
前置通知的執(zhí)行
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor
public Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
這里又會(huì)調(diào)用MethodInvocation.proceed()傳遞給下一個(gè)MethodInterceptor。
后置通知的執(zhí)行
org.springframework.aop.aspectj.AspectJAfterAdvice#invoke
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
finally {
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}
先執(zhí)行MethodInvocation.proceed(),最后在finally塊中調(diào)用后置通知的增強(qiáng),不管目標(biāo)方法有沒有拋出異常,finally代碼塊中的代碼都會(huì)執(zhí)行,也就是不管目標(biāo)方法有沒有拋出異常,后置通知都會(huì)執(zhí)行。
返回后通知的執(zhí)行
org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor#invoke
public Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}
先執(zhí)行MethodInvocation.proceed(),然后再執(zhí)行返回后通知的增強(qiáng)。
異常通知的執(zhí)行
org.springframework.aop.aspectj.AspectJAfterThrowingAdvice#invoke
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (Throwable ex) {
if (shouldInvokeOnThrowing(ex)) {
invokeAdviceMethod(getJoinPointMatch(), null, ex);
}
throw ex;
}
}
先執(zhí)行MethodInvocation.proceed(),如果目標(biāo)方法拋出了異常就會(huì)執(zhí)行異常通知的增強(qiáng),然后拋出異常,所以這時(shí)返回后通知的增強(qiáng)就不會(huì)執(zhí)行了。
總結(jié)各種通知的執(zhí)行順序:
Around begin // 環(huán)繞通知開始 Before // 前置通知 UserServiceImpl // 目標(biāo)方法的執(zhí)行 AfterReturning // 返回后通知 After // 后置通知 Around end // 環(huán)繞通知結(jié)束
到此這篇關(guān)于spring AOP原理及源碼分析的文章就介紹到這了,更多相關(guān)spring AOP原理源碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javaweb項(xiàng)目如何實(shí)現(xiàn)手機(jī)短信登錄
這篇文章主要介紹了javaweb項(xiàng)目如何實(shí)現(xiàn)手機(jī)短信登錄,手機(jī)號登錄在現(xiàn)在的項(xiàng)目中用的場景非常多,實(shí)現(xiàn)起來也不難,今天我們就一起來通過演示實(shí)現(xiàn)登錄過程,需要的朋友可以參考下2019-07-07
springboot實(shí)現(xiàn)rabbitmq的隊(duì)列初始化和綁定
這篇文章主要介紹了springboot實(shí)現(xiàn)rabbitmq的隊(duì)列初始化和綁定,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10
JAVA實(shí)現(xiàn)漢字轉(zhuǎn)拼音功能代碼實(shí)例
這篇文章主要介紹了JAVA實(shí)現(xiàn)漢字轉(zhuǎn)拼音功能代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
Java Swing JLabel標(biāo)簽的使用方法
這篇文章主要介紹了Java Swing JLabel標(biāo)簽的使用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Mybatis實(shí)現(xiàn)單個(gè)和批量定義別名typeAliases
這篇文章主要介紹了Mybatis實(shí)現(xiàn)單個(gè)和批量定義別名typeAliases,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Springboot基于assembly的服務(wù)化打包方案及spring boot部署方式
這篇文章主要介紹了Springboot基于assembly的服務(wù)化打包方案及springboot項(xiàng)目的幾種常見的部署方式,本文主要針對第二種部署方式提供一種更加友好的打包方案,需要的朋友可以參考下2017-12-12

