在Spring AOP中代理對(duì)象創(chuàng)建的步驟詳解
1. AOP 用法
先來(lái)一個(gè)簡(jiǎn)單的案例,小伙伴們先回顧一下 AOP,假設(shè)我有如下類(lèi):
@Service
public class UserService {
public void hello() {
System.out.println("hello javaboy");
}
}然后我寫(xiě)一個(gè)切面,攔截 UserService 中的方法:
@Component
@Aspect
@EnableAspectJAutoProxy
public class LogAspect {
@Before("execution(* org.javaboy.bean.aop.UserService.*(..))")
public void before(JoinPoint jp) {
String name = jp.getSignature().getName();
System.out.println(name+" 方法開(kāi)始執(zhí)行了...");
}
}最后,我們看一下從 Spring 容器中獲取到的 UserService 對(duì)象:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
UserService us = ctx.getBean(UserService.class);
System.out.println("us.getClass() = " + us.getClass());打印結(jié)果如下:

可以看到,獲取到的 UserService 是一個(gè)代理對(duì)象。
2. 原理分析
那么注入到 Spring 容器中的 UserService,為什么在獲取的時(shí)候變成了一個(gè)代理對(duì)象,而不是原本的 UserService 了呢?
整體上來(lái)說(shuō),我們可以將 Spring Bean 的生命周期分為四個(gè)階段,分別是:
- 實(shí)例化。
- 屬性賦值。
- 初始化。
- 銷(xiāo)毀。
如下圖:

首先實(shí)例化就是通過(guò)反射,先把 Bean 的實(shí)例創(chuàng)建出來(lái);接下來(lái)屬性賦值就是給創(chuàng)建出來(lái)的 Bean 的各個(gè)屬性賦值;接下來(lái)的初始化就是給 Bean 應(yīng)用上各種需要的后置處理器;最后則是銷(xiāo)毀。
2.1 doCreateBean
AOP 代理對(duì)象的創(chuàng)建是在初始化這個(gè)過(guò)程中完成的,所以今天我們就從初始化這里開(kāi)始看起。
AbstractAutowireCapableBeanFactory#doCreateBean:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
//...
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
//...
return exposedObject;
}小伙伴們看到,這里有一個(gè) initializeBean 方法,在這個(gè)方法中會(huì)對(duì) Bean 執(zhí)行各種后置處理器:
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex);
}
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}這里一共是執(zhí)行了四個(gè)方法,也都是非常常見(jiàn)的 Bean 初始化方法:
- invokeAwareMethods:執(zhí)行 Aware 接口下的 Bean。
- applyBeanPostProcessorsBeforeInitialization:執(zhí)行 BeanPostProcessor 中的前置方法。
- invokeInitMethods:執(zhí)行 Bean 的初始化方法 init。
- applyBeanPostProcessorsAfterInitialization:執(zhí)行 BeanPostProcessor 中的后置方法。
1、3 這兩個(gè)方法很明顯跟 AOP 關(guān)系不大,我們自己平時(shí)創(chuàng)建的 AOP 對(duì)象基本上都是在 applyBeanPostProcessorsAfterInitialization 中進(jìn)行處理的,我們來(lái)看下這個(gè)方法:
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}小伙伴們看到,這里就是遍歷各種 BeanPostProcessor,并執(zhí)行其 postProcessAfterInitialization 方法,將執(zhí)行結(jié)果賦值給 result 并返回。
2.2 postProcessAfterInitialization
BeanPostProcessor 有一個(gè)實(shí)現(xiàn)類(lèi) AbstractAutoProxyCreator,在 AbstractAutoProxyCreator 的 postProcessAfterInitialization 方法中,進(jìn)行了 AOP 的處理:
@Override
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;
}
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.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
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;
}可以看到,首先會(huì)嘗試去緩存中獲取代理對(duì)象,如果緩存中沒(méi)有的話,則會(huì)調(diào)用 wrapIfNecessary 方法進(jìn)行 AOP 的創(chuàng)建。
正常來(lái)說(shuō),普通 AOP 的創(chuàng)建,前面三個(gè) if 的條件都是不滿(mǎn)足的。第一個(gè) if 是說(shuō) beanName 是否是一個(gè) targetSource,顯然我們這里不是;第二個(gè) if 是說(shuō)這個(gè) Bean 是不是不需代理,我們這里顯然是需要代理的。
關(guān)于第二個(gè) if 我多說(shuō)一句,如果這里進(jìn)來(lái)的是一個(gè)切面的 Bean,例如第一小節(jié)中的 LogAspect,這種 Bean 顯然是不需要代理的,所以會(huì)在第二個(gè)方法中直接返回,如果是其他普通的 Bean,則第二個(gè) if 并不會(huì)進(jìn)來(lái)。
所在在 wrapIfNecessary 中,最重要的方法實(shí)際上就是兩個(gè):getAdvicesAndAdvisorsForBean 和 createProxy,前者用來(lái)找出來(lái)所有跟當(dāng)前類(lèi)匹配的切面,后者則用來(lái)創(chuàng)建代理對(duì)象。
2.3 getAdvicesAndAdvisorsForBean
這個(gè)方法,說(shuō)白了,就是查找各種 Advice(通知/增強(qiáng)) 和 Advisor(切面)。來(lái)看下到底怎么找的:
AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean:
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}從這里可看到,這個(gè)方法主要就是調(diào)用 findEligibleAdvisors 去獲取到所有的切面,繼續(xù):
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}這里一共有三個(gè)主要方法:
- findCandidateAdvisors:這個(gè)方法是查詢(xún)到所有候選的 Advisor,說(shuō)白了,就是把項(xiàng)目啟動(dòng)時(shí)注冊(cè)到 Spring 容器中所有切面都找到,由于一個(gè) Aspect 中可能存在多個(gè) Advice,每個(gè) Advice 最終都能封裝為一個(gè) Advisor,所以在具體查找過(guò)程中,找到 Aspect Bean 之后,還需要遍歷 Bean 中的方法。
- findAdvisorsThatCanApply:這個(gè)方法主要是從上個(gè)方法找到的所有切面中,根據(jù)切點(diǎn)過(guò)濾出來(lái)能夠應(yīng)用到當(dāng)前 Bean 的切面。
- extendAdvisors:這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來(lái),這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過(guò)調(diào)用 currentInvocation 方法取出即可。
接下來(lái)我們就來(lái)看一下這三個(gè)方法的具體實(shí)現(xiàn)。
2.3.1 findCandidateAdvisors
AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
@Override
protected List<Advisor> findCandidateAdvisors() {
List<Advisor> advisors = super.findCandidateAdvisors();
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}這個(gè)方法的關(guān)鍵在于通過(guò) buildAspectJAdvisors 構(gòu)建出所有的切面,這個(gè)方法有點(diǎn)復(fù)雜:
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
synchronized (this) {
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
if (!isEligibleBean(beanName)) {
continue;
}
// We must be careful not to instantiate beans eagerly as in this case they
// would be cached by the Spring container but would not have been weaved.
Class<?> beanType = this.beanFactory.getType(beanName, false);
if (beanType == null) {
continue;
}
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}
if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}這個(gè)方法第一次進(jìn)來(lái)的時(shí)候,aspectNames 變量是沒(méi)有值的,所以會(huì)先進(jìn)入到 if 分支中,給 aspectNames 和 aspectBeanNames 兩個(gè)變量賦值。
具體過(guò)程就是首先調(diào)用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors 方法,去當(dāng)前容器以及當(dāng)前容器的父容器中,查找到所有的 beanName,將返回的數(shù)組賦值給 beanNames 變量,然后對(duì) beanNames 進(jìn)行遍歷。
遍歷時(shí),首先調(diào)用 isEligibleBean 方法,這個(gè)方法是檢查給定名稱(chēng)的 Bean 是否符合自動(dòng)代理的條件的,這個(gè)細(xì)節(jié)我們就不看了,因?yàn)橐话闱闆r下,我們項(xiàng)目中的 AOP 都是自動(dòng)代理的。
接下來(lái)根據(jù) beanName,找到對(duì)應(yīng)的 bean 類(lèi)型 beanType,然后調(diào)用 advisorFactory.isAspect 方法去判斷這個(gè) beanType 是否是一個(gè) Aspect。
如果當(dāng)前 beanName 對(duì)應(yīng)的 Bean 是一個(gè) Aspect,那么就把 beanName 添加到 aspectNames 集合中,并且把 beanName 和 beanType 封裝為一個(gè) AspectMetadata 對(duì)象。
接下來(lái)會(huì)去判斷 kind 是否為 SINGLETON,這個(gè)默認(rèn)都是 SINGLETON,所以這里會(huì)進(jìn)入到分支中,進(jìn)來(lái)之后,會(huì)調(diào)用 this.advisorFactory.getAdvisors 方法去 Aspect 中找到各種通知和切點(diǎn)并封裝成 Advisor 對(duì)象返回,由于一個(gè)切面中可能定義多個(gè)通知,所以最終返回的 Advisor 是一個(gè)集合,最后把找到的 Advisor 集合存入到 advisorsCache 緩存中。
后面方法的邏輯就很好懂了,從 advisorsCache 中找到某一個(gè) aspect 對(duì)應(yīng)的所有 Advisor,并將之存入到 advisors 集合中,然后返回集合。
這樣,我們就找到了所有的 Advisor。
2.3.2 findAdvisorsThatCanApply
接下來(lái) findAdvisorsThatCanApply 方法主要是從眾多的 Advisor 中,找到能匹配上當(dāng)前 Bean 的 Advisor,小伙伴們知道,每一個(gè) Advisor 都包含一個(gè)切點(diǎn) Pointcut,不同的切點(diǎn)意味著不同的攔截規(guī)則,所以現(xiàn)在需要進(jìn)行匹配,檢查當(dāng)前類(lèi)需要和哪個(gè) Advisor 匹配:
protected List<Advisor> findAdvisorsThatCanApply(
List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
ProxyCreationContext.setCurrentProxiedBeanName(beanName);
try {
return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
}
finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}這里實(shí)際上就是調(diào)用了靜態(tài)方法 AopUtils.findAdvisorsThatCanApply 去查找匹配的 Advisor:
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new ArrayList<>();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
eligibleAdvisors.add(candidate);
}
}
boolean hasIntroductions = !eligibleAdvisors.isEmpty();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor) {
// already processed
continue;
}
if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}這個(gè)方法中首先會(huì)去判斷 Advisor 的類(lèi)型是否是 IntroductionAdvisor 類(lèi)型,IntroductionAdvisor 類(lèi)型的 Advisor 只能在類(lèi)級(jí)別進(jìn)行攔截,靈活度不如 PointcutAdvisor,所以我們一般都不是 IntroductionAdvisor,因此這里最終會(huì)走入到最后一個(gè)分支中:
public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
if (advisor instanceof IntroductionAdvisor ia) {
return ia.getClassFilter().matches(targetClass);
}
else if (advisor instanceof PointcutAdvisor pca) {
return canApply(pca.getPointcut(), targetClass, hasIntroductions);
}
else {
// It doesn't have a pointcut so we assume it applies.
return true;
}
}從這里小伙伴們就能看到,IntroductionAdvisor 類(lèi)型的 Advisor 只需要調(diào)用 ClassFilter 過(guò)濾一下就行了,小伙伴們看這里的匹配邏輯也是非常 easy!而 PointcutAdvisor 類(lèi)型的 Advisor 則會(huì)繼續(xù)調(diào)用 canApply 方法進(jìn)行判斷:
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}
IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher iamm) {
introductionAwareMethodMatcher = iamm;
}
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if (introductionAwareMethodMatcher != null ?
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}小伙伴們看一下,這里就是先按照類(lèi)去匹配,匹配通過(guò)則繼續(xù)按照方法去匹配,方法匹配器要是設(shè)置的 true,那就直接返回 true 就行了,否則就加載當(dāng)前類(lèi),也就是 targetClass,然后遍歷 targetClass 中的所有方法,最后調(diào)用 introductionAwareMethodMatcher.matches 方法去判斷方法是否和切點(diǎn)契合。
就這樣,我們就從所有的 Advisor 中找到了所有和當(dāng)前類(lèi)匹配的 Advisor 了。
2.3.3 extendAdvisors
這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來(lái),這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過(guò)調(diào)用 currentInvocation 方法取出即可。
這個(gè)方法的邏輯比較簡(jiǎn)單,我就不貼出來(lái)了,小伙伴們可以自行查看。
2.4 createProxy
看完了 getAdvicesAndAdvisorsForBean 方法,我們已經(jīng)找到了適合我們的 Advisor,接下來(lái)繼續(xù)看 createProxy 方法,這個(gè)方法用來(lái)創(chuàng)建一個(gè)代理對(duì)象:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false);
}
private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if (proxyFactory.isProxyTargetClass()) {
// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
}
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
classLoader = smartClassLoader.getOriginalClassLoader();
}
return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}好啦,經(jīng)過(guò)上面這一頓操作,代理對(duì)象就創(chuàng)建出來(lái)了~本文是一個(gè)大致的邏輯,還有一些特別細(xì)的小細(xì)節(jié)沒(méi)和小伙伴們梳理。
以上就是在Spring AOP中代理對(duì)象創(chuàng)建的步驟詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring AOP創(chuàng)建代理對(duì)象的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼具體代碼
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼具體代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
SpringBoot利用jackson格式化時(shí)間的三種方法
日常開(kāi)發(fā)過(guò)程中經(jīng)常會(huì)使用json進(jìn)行數(shù)據(jù)的傳輸,這就涉及到了對(duì)象和json的相互轉(zhuǎn)化,常用的解決方案有:Jackson(推薦)、谷歌的Gson、阿里的Fastjson,這篇文章主要給大家介紹了關(guān)于SpringBoot如何利用jackson格式化時(shí)間的相關(guān)資料,需要的朋友可以參考下2021-06-06
java7 簡(jiǎn)化變參方法調(diào)用實(shí)例方法
在本篇文章里我們給大家整理的是關(guān)于java7 簡(jiǎn)化變參方法調(diào)用實(shí)例方法以及實(shí)例代碼,需要的朋友們學(xué)習(xí)下。2019-11-11
Java+LibreOffice實(shí)現(xiàn)Excel轉(zhuǎn)PDF并橫向一頁(yè)顯示所有列
在實(shí)際業(yè)務(wù)場(chǎng)景中,用戶(hù)往往會(huì)提供格式不一的 Excel 文件,有時(shí)希望將其轉(zhuǎn)換為 PDF 并橫向顯示,所有列壓縮在一頁(yè)內(nèi),下面我們來(lái)看看具體實(shí)現(xiàn)方法吧2025-06-06
Java數(shù)組轉(zhuǎn)換為逗號(hào)分隔字符串的三種方法
這篇文章主要介紹了Java中三種將數(shù)組轉(zhuǎn)為逗號(hào)分隔字符串的方法:方法1通用但含括號(hào),方法2專(zhuān)用于字符串?dāng)?shù)組高效,方法3靈活但性能較差,建議根據(jù)數(shù)組類(lèi)型選擇String.join()或Arrays.toString(),需要的朋友可以參考下2025-09-09
spring?boot項(xiàng)目實(shí)戰(zhàn)之實(shí)現(xiàn)與數(shù)據(jù)庫(kù)的連接
在我們?nèi)粘5拈_(kāi)發(fā)過(guò)程中,肯定不可避免的會(huì)使用到數(shù)據(jù)庫(kù)以及SQL?語(yǔ)句,下面這篇文章主要給大家介紹了關(guān)于spring?boot項(xiàng)目實(shí)戰(zhàn)之實(shí)現(xiàn)與數(shù)據(jù)庫(kù)連接的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
java servlet結(jié)合Oracle搭建java的web開(kāi)發(fā)環(huán)境
今天我將與大家分享一下我學(xué)JAVA WEB寫(xiě)的一些小實(shí)例 ,我個(gè)人是不太喜歡書(shū)本上的晦澀的概念的,所以我花了更多的時(shí)間在一些應(yīng)用實(shí)例上,我覺(jué)得這樣的學(xué)習(xí)方式很適合我,由簡(jiǎn)到繁,由淺入深2015-12-12

