机缘巧合之下接触到了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有两种方案:
- 使用SpringBoot管理Jfinal的Filter,即通过SpringBoot去构造Jfinal服务,Jfinal的正常运行不需要SpringBoot的参与。此种方式可以称之为浅集成
- 使用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接口,需要以下步骤:
创建自定义Controller类,并继承com.jfinal.core.Controller,然后在其中编写方法,方法名就是请求路径。
-
创建自定义Routes类,并继承com.jfinal.config.Routes,然后通过add方法,加载自定义Controller,并设置路由路径。
如:
add("/admin", AdminController.clas)
然后通过com.jfinal.config.JFinalConfig的configRoute(Routes me)方法,加载自定义路由类。
配置数据源。虽然Jfinal提供了c3p0,druid,hikaricp等数据源配置插件,但是由于我们使用到了SpringBoot,没有必要去契合Jfinal内置的数据源插件了,我们自定义数据源插件,从Spring上下文中接收DataSource即可。
配置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 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 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 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
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!