如何利用Spring的@Import擴(kuò)展點(diǎn)與spring進(jìn)行無縫整合
利用Spring的@Import擴(kuò)展與spring進(jìn)行無縫整合前言BeanFactoryPostProcessor@Import實(shí)現(xiàn)POM文件定義數(shù)據(jù)層Resource(dao)層的掃描注解定義我的數(shù)據(jù)層Resource使用的注解ArteryResourceImportBeanDefinitionRegistrar實(shí)現(xiàn)自定義掃描類ClassPathArteryResourceScanner代理注冊工廠ResourceRegistryResouce的代理工廠真正的代理類方法調(diào)用類AbstractBeanDefinitionFactory我們編寫測試,來啟動我們的spring容器類圖
前言
spring有那些擴(kuò)展呢?
spring的擴(kuò)展非常多,比較常用的就是
BeanFactoryPostProcessor 我們可以插手spring bean工廠的初始化
BeanPostProcessor 我們可以插手spring bean實(shí)例化前后(比如SPRING AOP)
@Import
ImportAware。
BeanFactoryPostProcessor
spring的擴(kuò)展點(diǎn)之一BeanFactoryPostProcessor,這個學(xué)名叫spring的Bean工廠后置處理器,
它可以插手spring bean工廠的實(shí)例化,我們可以啟動spring的時候自己手動注冊一個bean工廠后置處理器,它能做的事情太多,研究過spring源碼的同學(xué)都知道,spring容器啟動時候,會先暴露一個工廠出來,這個工廠就是DefaultListableBeanFactory,這里面放置了我們的BeanDeinition,我們都知道spring 單例bean容器放了很多單例的bean,而這些bean最后都是來自于DefaultListableBeanFactory中的bd容器;
BeanFactoryPostProcessor是spring提供給我們來擴(kuò)展spring的,當(dāng)然了它自己也在用,spring有自己內(nèi)部的bean工廠后置處理器,處理的時候講我們的和spring自己的一起處理。我們只需要把我們新建的類實(shí)現(xiàn)了BeanFactoryPostProcessor,并且加入@Component或者交給@Import就可以了。實(shí)現(xiàn)這個接口必須實(shí)現(xiàn)它的一個方法,它的這個方法就可以得到我們的beanDefinittionMap,也就是bdmap,這里面放置了我們系統(tǒng)所有的注冊到spring容器里面的bd,最后spring循環(huán)這個bd,將其實(shí)例化成對象放入Bean容器。
今天我們的主題是使用sprinng的擴(kuò)展點(diǎn)之一的@Import來實(shí)現(xiàn)公司的平臺與spring整合,類似于Mybatis與spring整合一樣
@Import
這個要說就要說很久,如果沒有研究過spring底層源碼的,可以去研究下,功能非常強(qiáng)大這邊我大概介紹一下:
@import支持3中類型:
普通類(spring管理的類):就是講一個普通的類通過@import導(dǎo)入,而不適用@Component,但是這樣做毫無意義。
實(shí)現(xiàn)了ImportSelector:實(shí)現(xiàn)這個接口要求實(shí)現(xiàn)它的一個方法返回一個類名列表
Registrar:真正牛逼的注冊類,實(shí)現(xiàn)了它,我們可以手動往里面添加自己的BeanDefiniton,自己實(shí)現(xiàn)掃描機(jī)制,自己實(shí)現(xiàn)很多很多自己的邏輯(mybatis整合spring就用的它)
實(shí)現(xiàn)
我的工程命名是:xxx-spring-platform-1.0.REALSE
xxx是公司的簡稱
工程結(jié)構(gòu):

其中context是核心,core是一些常用的核心類,aop寫了一半,還沒完成
POM文件
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.2.7.RELEASE</version> </dependency>
就是用了srping的幾個基礎(chǔ)包
定義數(shù)據(jù)層Resource(dao)層的掃描注解
@Documented
@Retention(RUNTIME)
@Target(TYPE)
@Import(ArteryResourceImportBeanDefinitionRegistrar.class)
//這個是用了spring的@Import
//其中用了其擴(kuò)展點(diǎn)之一的@Import中的Registrar,ArteryResourceImportBeanDefinitionRegistrar才是我們的核心所在
public @interface ArteryResourceScan {
String[] value() default {};
/**
* 基礎(chǔ)包模式
*/
String[] basePackages() default {};
/**
* 通配符的模式
*/
String[] typeAliases() default {};
/**
* Artery Resource工廠Bean
*
* @return
*/
Class<? extends ArteryResourceFactoryBean> factoryBean() default ArteryResourceFactoryBean.class;
/**
* This property specifies the annotation that the scanner will search for.
* <p>
* The scanner will register all interfaces in the base package that also have
* the specified annotation
* </p>
*/
Class<? extends Annotation> annotationClass() default Annotation.class;
/**
* This property specifies the parent that the scanner will search for.
* <p>
* The scanner will register all interfaces in the base package that also have
* the specified interface class as a parent.
* </p>
*/
Class<?> markerInterface() default Class.class;
/**
* The property specifies the beanName gererator will extends parent
*/
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
/**
* is lazy load,default false
*/
boolean lazy() default false;
/**
* scope default singleton
*/
String scope() default AbstractBeanDefinition.SCOPE_SINGLETON;
}
定義我的數(shù)據(jù)層Resource使用的注解

ArteryResourceImportBeanDefinitionRegistrar實(shí)現(xiàn)
我們的注冊類實(shí)現(xiàn)了spring的即可ImportBeanDefinitionRegistrar,而ImportBeanDefinitionRegistrar提供了一個方法registerBeanDefinitions可以得到我們的ArteryResourceScan 注解,從而定義自己的掃描規(guī)則,使用spring的的掃描邏輯幫助我們完成掃描,然后注冊到bdmap里面,因我們的Resource層是接口,而spring實(shí)例化是不能實(shí)例化接口的,所以當(dāng)spring幫我們掃描成bd的時候,我們這個時候要這個掃描的列表取出來,替換我們的接口類,怎么替換呢?
因為接口需要被代理出去,而代理類幫我們完成我們想要做的事情,比如數(shù)據(jù)查詢,所以我們還需要定義一個工廠bean即FactoryBean,它來幫我們產(chǎn)生對象,F(xiàn)actoryBean也是一個Bean,但是他比較特殊,它可以產(chǎn)生對象,我們先看Registrar的實(shí)現(xiàn):
public class ArteryResourceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
private final String DEFAULT_RESOURACE_PATTERN = "**/*.class";
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ClassPathArteryResourceScanner scanner = new ClassPathArteryResourceScanner(registry);
/**
* 拿到注解信息
*/
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(ArteryResourceScan.class.getName()));
Class<? extends ArteryResourceFactoryBean> factoryBean = annoAttrs.getClass("factoryBean");
if (!ArteryResourceFactoryBean.class.equals(factoryBean)) {
scanner.setArteryResourceFactoryBean(BeanUtils.instantiateClass(factoryBean));
}
Class<? extends Annotation> annotionClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotionClass)) {
scanner.setAnnotationClass(annotionClass);
}
Class<?> markerInteface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInteface)) {
scanner.setMarkerInterface(markerInteface);
}
Class<? extends BeanNameGenerator> nameGenerator = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(nameGenerator)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(nameGenerator));
}
scanner.setLazy(annoAttrs.getBoolean("lazy"));
scanner.setResourceScope(annoAttrs.getString("scope"));
//base package handler
List<String> basePackages = new ArrayList<>(20);
Arrays.asList(annoAttrs.getStringArray("basePackages")).forEach(item -> {
if (StringUtils.hasText(item)) {
basePackages.add(item);
}
});
Arrays.asList(annoAttrs.getStringArray("value")).forEach(item -> {
if (StringUtils.hasText(item)) {
basePackages.add(item);
}
});
/**
* 處理通配符的掃描問題
*/
String[] typeAlis = annoAttrs.getStringArray("typeAliases");
if (typeAlis != null && typeAlis.length > 0) {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
//typeAliaes
for (String typeAliases : Arrays.asList(annoAttrs.getStringArray("typeAliases"))) {
getResource(resolver, metadataReaderFactory, typeAliases, basePackages);
}
}
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(basePackages));
}
private void getResource(ResourcePatternResolver resolver, MetadataReaderFactory metadataReaderFactory, String classPath, List<String> basePackages) {
classPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(classPath) + "/" + DEFAULT_RESOURACE_PATTERN;
try {
Resource[] resources = resolver.getResources(classPath);
if (resources != null && resources.length > 0) {
MetadataReader metadataReader = null;
for (Resource resource : resources) {
if (resource.isReadable()) {
metadataReader = metadataReaderFactory.getMetadataReader(resource);
basePackages.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
}
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
以上代碼的doScan方法是核心,是調(diào)用了我們自定義的掃描類
自定義掃描類ClassPathArteryResourceScanner
public class ClassPathArteryResourceScanner extends ClassPathBeanDefinitionScanner {
private final Logger logger = LoggerFactory.getLogger(ClassPathArteryResourceScanner.class);
/**
* factory baen instance
*/
private ArteryResourceFactoryBean<?> arteryResourceFactoryBean = new ArteryResourceFactoryBean<Object>();
/**
* scanner class
*/
private Class<? extends Annotation> annotationClass;
/**
* scanner class
*/
private Class<?> markerInterface;
/**
* is lazy load ,default false
*/
private boolean isLazy = false;
/**
* scope is cantains singleton and prototype,default singlton
*/
private String resourceScope = AbstractBeanDefinition.SCOPE_SINGLETON;
/**
* 調(diào)用父類的構(gòu)造,構(gòu)造出掃描對象
*
* @param registry
*/
public ClassPathArteryResourceScanner(BeanDefinitionRegistry registry) {
super(registry, false);
}
public void registerFilters() {
//是否允許所有的所有的接口(預(yù)留)
boolean acceptAllIntefaces = true;
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllIntefaces = false;
}
if (this.markerInterface != null) {
addIncludeFilter(new AssignableTypeFilter(this.markerInterface));
acceptAllIntefaces = false;
}
if (acceptAllIntefaces) {
addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
}
}
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (!beanDefinitions.isEmpty()) {
//SPRING 掃描到每個加了@Component或者@Service 成BD
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
beanDefinitions.forEach(this::processBeanDefinition);
}
private void processBeanDefinition(BeanDefinitionHolder holder) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating ArteryResourceBean with name {} and {} mapperInterfaces", holder.getBeanName(), definition.getBeanClassName());
}
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.arteryResourceFactoryBean.getClass());
definition.setLazyInit(isLazy);//延遲加載
definition.setScope(resourceScope);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
@Override
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
// check is sucess
boolean isSucc = true;
if (super.checkCandidate(beanName, beanDefinition)) {
isSucc = true;
} else {
isSucc = false;
}
return isSucc;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
boolean isComponent = false;
String beanClassName = beanDefinition.getMetadata().getClassName();
isComponent = beanClassName.endsWith("Resource");
try {
isComponent = isComponent ? Class.forName(beanDefinition.getMetadata().getClassName()).isAnnotationPresent(ArteryDao.class) : false;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (isComponent) {
isComponent = beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
return isComponent;
}
public void setArteryResourceFactoryBean(ArteryResourceFactoryBean<?> arteryResourceFactoryBean) {
this.arteryResourceFactoryBean = arteryResourceFactoryBean;
}
public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
this.annotationClass = annotationClass;
}
public void setLazy(boolean lazy) {
isLazy = lazy;
}
public void setResourceScope(String resourceScope) {
this.resourceScope = resourceScope;
}
public void setMarkerInterface(Class<?> markerInterface) {
this.markerInterface = markerInterface;
}
}
processBeanDefinition這個方法里面就拿到spring給我們掃描返回的bd,我們循環(huán)這個bd
然后替換我們的Resource接口,這里用的是一個FactoryBean
而這個工廠Bean里面的getObject是返回了一個代理對象,具體看下面代碼:

代理注冊工廠ResourceRegistry

它的作用主要是來管理我們的注冊工廠
Resouce的代理工廠

它來管理我們的Resouce工廠,從這個Resource工廠中產(chǎn)生代理類,也就是我們的代理類都在代理工廠中產(chǎn)生,然后我們調(diào)用的時候是通過它來產(chǎn)生的一個proxy
真正的代理類
public class ResourceProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = 1L;
private final Logger logger = LoggerFactory.getLogger(ResourceProxy.class);
private final Class<T> resourceInterface;
private final Map<Method, ResourceMethod> cacheMethod;
public ResourceProxy(Class<T> resourceInterface, Map<Method, ResourceMethod> cacheMethod) {
this.resourceInterface = resourceInterface;
this.cacheMethod = cacheMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
/**
* handler tostring method
*/
if (Object.class.equals(method.getDeclaringClass())) {
result = method.invoke(this, args);
} else if (isDefaultMethod(method)) {
result = invokeDefaultMethod(proxy, method, args);
} else {
/**
* user invoke handler
*/
if (logger.isDebugEnabled()) {
logger.debug("ResourceProxy.invoke begin ....");
logger.debug("================================================");
logger.debug("invoke interface name={}", proxy.getClass().getInterfaces()[0].getName());
logger.debug("invoke method name={}", method.getName());
logger.debug("invoke method args={}", args);
}
ResourceMethod resourceMethod = getCacheMethod(method);
result = resourceMethod.execute(args);
if (logger.isDebugEnabled()) {
logger.debug("ResourceProxy.invoke end ....");
logger.debug("================================================");
logger.debug("invoke method result={}", result);
}
}
return result;
}
public ResourceMethod getCacheMethod(Method method) {
ResourceMethod resourceMethod = cacheMethod.get(method);
if (resourceMethod == null) {
resourceMethod = new ResourceMethod(method, resourceInterface);
cacheMethod.put(method, resourceMethod);
}
return resourceMethod;
}
/**
* invoke default method
*
* @param proxy proxy object
* @param method proxy invoke method
* @param args method args
* @return
* @throws Throwable
*/
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* check is default method
*
* @param method
* @return
*/
private boolean isDefaultMethod(Method method) {
return ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC) && method.getDeclaringClass().isInterface();
}
}
方法調(diào)用類
public class ResourceMethod<T> extends AbstractAnnotaionHandlerResource<T> {
private final Method method;
private final Class<?> resourceInterface;
public ResourceMethod(Method method, Class<?> resourceInterface) {
this.method = method;
this.resourceInterface = resourceInterface;
}
public Object execute(Object[] args) throws Exception {
Object result = null;
initTemplate();
if (method.isAnnotationPresent(PubHandler.class)) {
result = pubHandler(args);
} else {
result = invokeExtMethod(args);
}
return result;
}
private void initTemplate(){
if(queryTemplate == null){
queryTemplate = SpringContainerApplicationContext.getInstance().getBean("queryTemplate");
}
if(updateTemplate == null){
updateTemplate =SpringContainerApplicationContext.getInstance().getBean("updateTemplate");
}
}
private Object invokeExtMethod(Object[] args) throws Exception {
Object result = null;
Annotation[] annotations = method.getAnnotations();
if (annotations != null && annotations.length > 0) {
String annotationName = annotations[0].annotationType().getSimpleName();
SqlCommandType type = SqlCommandType.valueOf(annotationName);
switch (type) {
case SELECT:
result = executeQuery(method.getAnnotation(SELECT.class), args);
break;
case INSERT:
result = executeInsert(method.getAnnotation(INSERT.class), args);
break;
case DELETE:
result = executeDelete(method.getAnnotation(DELETE.class), args);
break;
case UPDATE:
result = executeUpdate(method.getAnnotation(UPDATE.class), args);
break;
}
}
return result;
}
private Object pubHandler(Object[] args) {
Object result = null;
PubHandler ph = method.getAnnotation(PubHandler.class);
switch (ph.handlerType()) {
case P_Q_PAGING:
result = P_Q_PAGING(args);
break;
case L_Q_PAGING:
result = L_Q_PAGING(args);
break;
case I_PERSISTENCE_IN:
result = I_PERSISTENCE_IN(args);
break;
case I_PERSISTENCE_UP:
result = I_PERSISTENCE_UP(args);
break;
case I_PERSISTENCE_UP_OVERRIDE:
result = I_PERSISTENCE_UP_OVERRIDE(args);
break;
case I_PSERSISTENCE_DE:
result = I_PSERSISTENCE_DE(args);
break;
case I_PSERSISTENCE_DE_OVERRIDE:
result = I_PSERSISTENCE_DE_OVERRIDE(args);
break;
case E_Q_GET:
result = E_Q_GET(args);
break;
case L_Q_ENTITY:
result = L_Q_ENTITY(args);
break;
case L_Q_ENTITY_OVERRIDE:
result = L_Q_ENTITY_OVERRIDE(args);
break;
case I_BATCH_PERSISTENCE:
result = I_BATCH_PERSISTENCE(args);
break;
case I_COUNT:
result = I_COUNT(args);
break;
}
return result;
}
@Override
public Method getMethod() {
return method;
}
@Override
public Class<?> targetInterface() {
return resourceInterface;
}
從第七步調(diào)用直接到了這里,其中1和2是不一樣的,哪里不一樣呢?因為pubHandler是我們在基類里面封裝的curd操作,如果是調(diào)用基類,那么會直接將請求代理給我的基類去做,如果是通過注解sql的那么就執(zhí)行invokeExtMethod


最后我們編寫一個工廠來獲取我們的Bean
AbstractBeanDefinitionFactory
public abstract class AbstractBeanDefinitionFactory extends AnnotationConfigDefinitionApplicationContext
implements BeanDefinitionFactory {
@SuppressWarnings("unchecked")
@Override
public <K> K getBean(String beanName) {
check();
DefinitionBeanFactory<K> beanfactory = () -> {
Object beanInstance = super.applicationContxt.getBean(beanName);
if (beanInstance != null) {
return (K) beanInstance;
} else {
throw new RuntimeException(" get spring ioc instance is null ");
}
};
return beanfactory.getBeanObject();
}
@Override
public <K> K getBean(Class<K> kclzz) {
check();
DefinitionBeanFactory<K> beanfactory = () -> super.applicationContxt.getBean(kclzz);
return beanfactory.getBeanObject();
}
private void check() {
if(!super.runStatus) {
throw new RuntimeException("spring 容器未運(yùn)行");
}
}
@Override
public <K> K getBean(String beanName, Class<K> kClass) {
check();
DefinitionBeanFactory<K> beanFactory = () -> super.applicationContxt.getBean(beanName,kClass);
return null;
}
}
我們編寫測試,來啟動我們的spring容器



以上我只是提供了一個spring擴(kuò)展的思路,上面截圖和代碼都不全,因為涉及到公司機(jī)密性,我沒有辦法暴露太多東西在上面,所以有興趣可以一起交流交流;整合spring的這個框架是我自己編寫,沒有任何人參與進(jìn)來,所以我希望如果有這方面興趣的朋友可以一起交流交流
類圖
我的啟動類的之間關(guān)系(全部是自己的類,不是spring的類):

代理工廠調(diào)用的調(diào)用的方法處理繼承關(guān)系:

最后預(yù)祝我的兩個小公主一直開心快樂,身體永遠(yuǎn)健康,爸爸永遠(yuǎn)愛你們。
到此這篇關(guān)于如何利用Spring的@Import擴(kuò)展點(diǎn)與spring進(jìn)行無縫整合的文章就介紹到這了,更多相關(guān)Spring的@Import擴(kuò)展點(diǎn)與spring無縫整合內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用java采集京東商城區(qū)劃數(shù)據(jù)示例
這篇文章主要介紹了java采集京東的全國區(qū)劃數(shù)據(jù)示例,保存成json形式,如想轉(zhuǎn)換到數(shù)據(jù)庫只需反序列化為對象保存到數(shù)據(jù)庫即可2014-03-03
java多態(tài)實(shí)現(xiàn)電子寵物系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java多態(tài)實(shí)現(xiàn)電子寵物系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
Java動態(tài)規(guī)劃之丑數(shù)問題實(shí)例講解
這篇文章主要介紹了Java動態(tài)規(guī)劃之丑數(shù)問題實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09
idea2020安裝MybatisCodeHelper插件的圖文教程
這篇文章主要介紹了idea2020安裝MybatisCodeHelper插件的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
intellij idea設(shè)置統(tǒng)一JavaDoc模板的方法詳解
這篇文章主要介紹了intellij idea設(shè)置統(tǒng)一JavaDoc模板的方法詳解,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04

