SpringBoot完美整合Jfinal

作者: 梦中一点心雨 | 来源:发表于2019-08-04 18:29 被阅读243次

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

    环境准备

    引入SpringBoot、Jfinal等相关配置

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.1.6.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
        <groupId>com.jfinal</groupId>
        <artifactId>jfinal</artifactId>
        <version>3.8</version>
      </dependency>
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
      </dependency>
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
      </dependency>
    </dependencies>
    

    集成方案

    使用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<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    
        Class<? extends Annotation> 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<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
        Class<? extends Annotation> 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<? extends Annotation> 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<BeanDefinitionHolder> doScan(String... basePackages) {
            Set<BeanDefinitionHolder> 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<? extends Annotation> 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<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
            if (!BeanNameGenerator.class.equals(generatorClass)) {
                scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
            }
            List<String> packages = new ArrayList<String>();
            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<String, Controller> 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<Resource> resources = new ArrayList<Resource>();
      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
    我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!


    微信公众号1

    相关文章

      网友评论

        本文标题:SpringBoot完美整合Jfinal

        本文链接:https://www.haomeiwen.com/subject/hqiydctx.html