SpringBoot完美整合Jfinal

机缘巧合之下接触到了Jfinal,但是由于个人目前主要的技术栈还是以SpringBoot为主,所以不免得想着将Jfinal与SpringBoot集成到一起去使用。

环境准备

引入SpringBoot、Jfinal等相关配置


  org.springframework.boot
  spring-boot-starter-parent
  2.1.6.RELEASE
   


  
    org.springframework.boot
    spring-boot-starter-web
  
  
    com.jfinal
    jfinal
    3.8
  
  
    mysql
    mysql-connector-java
    5.1.47
  
  
    org.springframework.boot
    spring-boot-starter-jdbc
  
  
    org.springframework.boot
    spring-boot-configuration-processor
  
  
    org.projectlombok
    lombok
  

集成方案

使用SpringBoot集成Jfinal有两种方案:

  1. 使用SpringBoot管理Jfinal的Filter,即通过SpringBoot去构造Jfinal服务,Jfinal的正常运行不需要SpringBoot的参与。此种方式可以称之为浅集成
  2. 使用SpringBoot管理Jfinal的Routes、Controller、Interceptor等,SpringBoot与Jfinal混合交叉使用,即在Jfinal的Bean中,可使用SpringBoot的其他Bean。可称之为深度集成。

注: Jfinal和SpringBoot的项目搭建不做介绍,读者可自行学习。

SpringBoot与Jfinal浅集成

编写SpringJFinalFilter过滤器

public class SpringJFinalFilter implements Filter {
    private Filter jfinalFilter;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        jfinalFilter = createJFinalFilter("com.jfinal.core.JFinalFilter");
        jfinalFilter.init(filterConfig);
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        jfinalFilter.doFilter(request, response, chain);
    }
    @Override
    public void destroy() {
        jfinalFilter.destroy();
    }
    private Filter createJFinalFilter(String filterClass) {
        Object temp = null;
        try {
            temp = Class.forName(filterClass).newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Can not create instance of class: " + filterClass, e);
        }

        if (temp instanceof Filter) {
            return (Filter) temp;
        } else {
            throw new RuntimeException("Can not create instance of class: " + filterClass + ".");
        }
    }
}

配置Filter

@Bean
public FilterRegistrationBean filterRegistrationBean() {
  FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
  filterRegistrationBean.setFilter(new SpringJFinalFilter());
  filterRegistrationBean.addUrlPatterns("/*");
  return filterRegistrationBean;
}

其他使用Jfinal原生配置,不需要掺杂SpringBoot的任何配置项,即可由SpringBoot启动Jfinal应用。此种方式简单明了,但是可用性不高,虽然Jfinal依赖于SpringBoot在运行,但是两个框架由互相独立,未做到交叉使用。故需要接下来的SpringBoot与Jfinal的深度集成。

SpringBoot与Jfinal深度集成

SpringBoot与Jfinal的深度集成,将SpringBoot的Routes、Controller、Interceptor作为Spring的Bean来处理,以便与SpringBoot以及其他框架交叉使用,来满足不同的需求。

如果使用Jfinal来开发后台REST接口,需要以下步骤:

  1. 创建自定义Controller类,并继承com.jfinal.core.Controller,然后在其中编写方法,方法名就是请求路径。

  2. 创建自定义Routes类,并继承com.jfinal.config.Routes,然后通过add方法,加载自定义Controller,并设置路由路径。

    如:add("/admin", AdminController.clas)

  3. 然后通过com.jfinal.config.JFinalConfig的configRoute(Routes me)方法,加载自定义路由类。

  4. 配置数据源。虽然Jfinal提供了c3p0,druid,hikaricp等数据源配置插件,但是由于我们使用到了SpringBoot,没有必要去契合Jfinal内置的数据源插件了,我们自定义数据源插件,从Spring上下文中接收DataSource即可。

  5. 配置SQL模板路径。SpringBoot项目线上运行时,基本采用的是jar包方式运行,通过classpath,去获取文件的相对路径获取SQL模板,在打成jar包之后会报找不到对应的文件的错误,所以需要改成以流的方式加载SQL模板。

模仿MyBatis的MapperScan功能

新建两个注解:

BeanScan.java

/**
 * 在指定包下扫面标志类,将其加载到Spring上下文中
 * @author chenmin
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(JfinalControlScannerRegistrar.class)
public @interface BeanScan {
    /**
     * 包扫描路径(不填时从当前路径下扫描)
     * @return
     */
    String[] basePackages() default {};

    Class[] basePackageClasses() default {};

    Class nameGenerator() default BeanNameGenerator.class;

    Class annotationClass() default Annotation.class;
    /**
     * 标识类列表
     * @return
     */
    Class[] markerInterfaces() default {};
}

RouterPath.java

/**
 * 定义Jfinal Controller路由
 * @author chenmin
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RouterPath {
    /**
     * 路由值
     * @return
     */
    String value() default "";
}

既然已经有现有框架有这种功能了,那我们直接去看MyBatis的源码,快速了解MyBatis的源码的同时还能熟悉Spring的源码呢。

在MyBatis的MapperScan注解所在包中,存在三个关键类:ClassPathMapperScanner,MapperScannerRegistrar,MapperScannerConfigurer。其中MapperScannerConfigurer配置类是为了整合Spring和MyBatis所存在的,我们不去关注它。我们重点看另外两个类,参考它们实现我们自定义的工具,MyBatis的源码不做解释,因为原理类似,在实现自定义工具时,会顺带着讲解这部分原理。

ClassPathMapperScanner的父类ClassPathBeanDefinitionScanner,它的作用就是将指定包下的类通过一定规则过滤后 将Class 信息包装成 BeanDefinition 的形式注册到IOC容器中。

MapperScannerRegistrar的ImportBeanDefinitionRegistrar接口不是直接注册Bean到IOC容器,它的执行时机比较早,准确的说更像是注册Bean的定义信息以便后面的Bean的创建。

BeanScan.java

/**
 * 在指定包下扫面标志类,将其加载到Spring上下文中
 * @author chenmin
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(JfinalControlScannerRegistrar.class)
public @interface BeanScan {
    /**
     * 包扫描路径(不填时从当前路径下扫描)
     * @return
     */
    String[] basePackages() default {};
    Class nameGenerator() default BeanNameGenerator.class;
    Class annotationClass() default Annotation.class;
    /**
     * 标识类
     * @return
     */
    Class[] markerInterfaces() default {};
}

RouterPath.java

/**
 * 定义Jfinal Controller路由
 * @author chenmin
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RouterPath {
    /**
     * 路由值
     * @return
     */
    String value() default "";
}

ClassPathJfinalControlScanner.java

@Slf4j
@Data
@Accessors(chain = true)
public class ClassPathJfinalControlScanner extends ClassPathBeanDefinitionScanner {
    private Class annotationClass;
    private Class[] markerInterfaces;
    public ClassPathJfinalControlScanner(BeanDefinitionRegistry registry) {
        super(registry, false);
    }
    /**
     * 配置扫描接口
     * 扫描添加了markerInterfaces标志类的类或标注了annotationClass注解的类,
     * 或者扫描所有类
     */
    public void registerFilters() {
        if (this.annotationClass != null) {
            addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
        }
        if (this.markerInterfaces != null) {
            for (Class markerInterface : markerInterfaces) {
                addIncludeFilter(new AssignableTypeFilter(markerInterface));
            }
        }
    }
    /**
     * 重写ClassPathBeanDefinitionScanner的doScan方法,以便在我们自己的逻辑中调用
     * @param basePackages
     * @return
     */
    @Override
    protected Set doScan(String... basePackages) {
        Set beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            log.warn("No Jfinal Controller was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        }
        return beanDefinitions;
    }
      /**
     * 判断bean是否满足条件,可以被加载到Spring中,markerInterfaces标志类功能再此处实现
     * @param beanDefinition
     * @return true: 可以被加载到Spring中
     */
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        Boolean flag = false;
        for (Class markerInterface : markerInterfaces) {
            flag = markerInterface.getName().equals(beanDefinition.getMetadata().getSuperClassName());
            if (!flag) {
                String[] interfaceNames = beanDefinition.getMetadata().getInterfaceNames();
                for (String interfaceName : interfaceNames) {
                    flag = markerInterface.getName().equals(interfaceName);
                    if (flag) {
                        return flag;
                    }
                }
            }
            if (flag) {
                return flag;
            }
        }
        return flag;
    }
}

JfinalControlScannerRegistrar.java

@Slf4j
@Data
@Accessors(chain = true)
public class JfinalControlScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware, BeanFactoryAware {
    private ResourceLoader resourceLoader;
    private Environment environment;
    private BeanFactory beanFactory;
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(BeanScan.class.getName()));
        ClassPathJfinalControlScanner scanner = new ClassPathJfinalControlScanner(registry);
        // this check is needed in Spring 3.1
        if (resourceLoader != null) {
            scanner.setResourceLoader(resourceLoader);
        }
        Class annotationClass = annoAttrs.getClass("annotationClass");
        if (!Annotation.class.equals(annotationClass)) {
            scanner.setAnnotationClass(annotationClass);
        }
        Class[] markerInterfaces = annoAttrs.getClassArray("markerInterfaces");
        if (!Class.class.equals(markerInterfaces)) {
            scanner.setMarkerInterfaces(markerInterfaces);
        }
        Class generatorClass = annoAttrs.getClass("nameGenerator");
        if (!BeanNameGenerator.class.equals(generatorClass)) {
            scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
        }
        List packages = new ArrayList();
        String[] basePackages = annoAttrs.getStringArray("basePackages");
        if (ObjectUtils.isEmpty(basePackages)) {
            packages.addAll(AutoConfigurationPackages.get(this.beanFactory));
        } else {
            packages.addAll(Arrays.asList(basePackages));
        }
        scanner.registerFilters();
        scanner.doScan(StringUtils.toStringArray(packages));
    }
}

工具写好之后,将自定义的RouterPath注解标注到Controller上,value为controller路由。

@RouterPath("/achievement")
public class AdminAchievementController extends Controller {
    public void index() {
    renderJson("Hello");
  }
}

新建DefaultRouter.java

/**
 * 通过applicationContext.getBeansOfType(Controller.class)获取所有Jfinal的Controller,并获取对应 * @RouterPath注解值
 */
@Component
public class DefaultRouter extends Routes {
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private DefaultInterceptor interceptor;
    @Override
    public void config() {
        addInterceptor(interceptor);
        Map controllerMap = applicationContext.getBeansOfType(Controller.class);
        if (!ObjectUtils.isEmpty(controllerMap)) {
            controllerMap.values().forEach(controller -> {
                String value = "";
                RouterPath annotation = controller.getClass().getAnnotation(RouterPath.class);
                if (!ObjectUtils.isEmpty(annotation)) {
                    value = annotation.value();
                }
                if (ObjectUtils.isEmpty(value)) {
                    String simpleName = controller.getClass().getSimpleName();
                    value = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
                }
                add(value, controller.getClass());
            });
        }
    }
}

自定义Jfinal数据源配置Plugins

新建SpringDataSourceCpPlugin.java

/**
 * 自定义Jfinal DataSourcePlugin,从Spring上下文中注入DataSource,不需要自己去获取数据源属性定义数据源
 * 了.
 */
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@Component
public class SpringDataSourceCpPlugin implements IPlugin, IDataSourceProvider {
    @Autowired
    private DataSource dataSource;
    @Override
    public boolean start() {
        if (ObjectUtils.isEmpty(dataSource)) {
            return false;
        }
        return true;
    }
    @Override
    public boolean stop() {
        if (dataSource != null) {
            dataSource = null;
        }
        return true;
    }
}

然后通过JFinalConfig的configPlugin方法,将自定义的数据源Plugin加入进去。

JFinal SQL模板路径配置化

在application.yml中添加配置项:

jfinal:
  template: classpath:template/*/*.sql

加载SQL模板文件(只贴出来了关键代码)

private void getSqlTemplates(ActiveRecordPlugin arp) {
  ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
  List resources = new ArrayList();
  String template = this.jfinalProperties.getTemplate();
  if (template != null) {
    try {
      Resource[] sqlTemplates = resourceResolver.getResources(template);
      resources.addAll(Arrays.asList(sqlTemplates));
    } catch (IOException e) {
      // ignore
    }
  }
  resources.forEach(resource -> {
    StringBuilder content = null;
    try {
      content = getContentByStream(resource.getInputStream());
      arp.addSqlTemplate(new StringSource(content, true));
    } catch (IOException e) {
      e.printStackTrace();
    }
  });
}
private StringBuilder getContentByStream(InputStream inputStream) {
  StringBuilder stringBuilder = new StringBuilder();
  try {
    BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    String line;
    while ((line = br.readLine()) != null) {
      stringBuilder.append(line);
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
  return stringBuilder;
}

至此,SpringBoot与Jfinal完美集成到一起,在Jfinal的Bean中也可以正常使用Spring的所有功能。


关注我的微信公众号:FramePower
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!


SpringBoot完美整合Jfinal_第1张图片
微信公众号1

你可能感兴趣的:(SpringBoot完美整合Jfinal)