The introduction

Yesterday when I was looking at the server container, I came across JFinal by accident. Before, my impression of JFinal was only that it was a framework developed by Chinese people to integrate the Spring family bucket.

Then I looked it up, and it didn’t seem that simple.

JFinal has been the best open source project in OSChina for many years. It is not the integration of Spring buckets as I previously understood, but the development of a WEB + ORM + AOP + Template Engine framework.

First look at the official warehouse on their own introduction:

Save more time to spend with loved ones, family and friends :).

Do code farming this line, who does not want to finish the work early, can leave work normally, rather than 996 blessings every day.

Given that such an excellent framework has never been understood by myself, this is definitely something that a Java veteran can’t tolerate.

So today I’m going to do an out-of-the-box review of the framework to see if it can save as much time as advertised and if it works.

This may be the first time in the industry to do the framework evaluation of the article, or first low-key: MY ability is limited, if the following content is wrong, please forgive me.

The next goal is to do a simple Demo and perform the simplest CRUD operations to experience JFinal.

Build the project

I opened the official JFinal document with reverence.

  • Document address: jfinal.com/doc

When I saw the sample project on the official website, I had to go down and have a look at it. At this moment, something completely unexpected happened to me. I was asked to register and log in.

All right, all right, you’re the boss and you call the shots, and I’m craving your body.

The official construction demo of the project is using Eclipse, well, you win again, I use IDEA to follow your steps.

The process is as simple as creating a Maven project and importing dependencies. The core dependencies are the following:

<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal-undertow</artifactId>
    <version>2.1</version>
</dependency>

<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal</artifactId>
    <version>4.9</version>
</dependency>
Copy the code

I won’t post the full code (it’s too long after all), but the code will be submitted to the code repository, which can be accessed by interested students.

In fact, SpringBoot is used to the process of creating projects, has been very not used to using this way to build projects, exclude IDEA support for SpringBoot project construction, directly access start.spring. IO /, Directly check the box to select the dependency you need to directly download into the IDE.

However, there is no need to talk about this, SpringBoot is backed by a large team, and JFinal seems to have only one developer, which is basically the pride of the people in the open source field.

Project start

With project dependencies in place, the first thing to do is to start the project. In JFinal, there is a global configuration class, and the code to start the project is there.

This class needs to inherit JFinalConfig, which in turn implements the following six abstract methods:

public class DemoConfig extends JFinalConfig {
    public void configConstant(Constants me) {}
    public void configRoute(Routes me) {}
    public void configEngine(Engine me) {}
    public void configPlugin(Plugins me) {}
    public void configInterceptor(Interceptors me) {}
    public void configHandler(Handlers me) {}}Copy the code

configConstant

This method is used to configure constants for JFinal, such as cglib for aop proxies, SLF4J for logging, utF-8 for default encoding, and so on.

Here are some configurations from the official documentation I chose:

public void configConstant(Constants me) {
    // Configure the development mode. The true value is development mode
    me.setDevMode(true);
    // Configure the AOP proxy to use Cglib, otherwise the jFinal default dynamically compiled proxy scheme will be used
    me.setToCglibProxyFactory();
    // Configure dependency injection
    me.setInjectDependency(true);
    // Whether to inject the superclass of the injected class when configuring dependency injection
    me.setInjectSuperClass(false);
    // Configure the slf4J logging system, otherwise log4j will be used by default
    // You can also use me.setLogFactory(...). A logging system implementation class configured to extend itself
    me.setToSlf4jLogFactory();
    // Set the Json conversion factory implementation class, more explained in Chapter 12
    me.setJsonFactory(new MixedJsonFactory());
    // Configure the view type. The jFinal Enjoy template engine is used by default
    me.setViewType(ViewType.JFINAL_TEMPLATE);
    // Configure pages 404 and 500
    me.setError404View("/common/404.html");
    me.setError500View("/common/500.html");
    // Set encoding, default is UTF8
    me.setEncoding("UTF8");
    // Configure the data parttern used when converting the Date type
    me.setJsonDatePattern("yyyy-MM-dd HH:mm");
    RenderJsp (xxx.jsp) is not allowed to access the.jsp file
    me.setDenyAccessJsp(true);
    // Set the maximum amount of data to be uploaded. The default value is 10 MB
    me.setMaxPostSize(10 * 1024 * 1024);
    // Configure the urlPara delimiter. Default is "-".
    me.setUrlParaSeparator("-");
}
Copy the code

Here is some general configuration information for some projects, which in SpringBoot is usually written in the YAML or Property configuration file, but I don’t personally feel comfortable with this configuration, just a little uncomfortable with it.

configRoute

This method configures the access routing information. My example looks like this:

public void configRoute(Routes me) {
    me.add("/user", UserController.class);
}
Copy the code

One of the things that occurs to me is that every time I add a new Controller I have to come here and configure routing information, it’s just silly.

If it is a small project, it is good that the routing information is not very much, there are a dozen or several dozen is enough. If it is some medium and large projects, hundreds or thousands of controllers, if I configure them here, can I find them? There is a question mark here.

There is a fatal problem in practical application. At the time of release, those of you who have done projects know that there are at least four environments: development, test, UAT, and production. Each environment has a different version of the code function, do I need to manually modify this before release? How can this be managed?

configEngine

This is used to configure the Template Engine, also known as the page Template. Since I only want to write two Restful interfaces, I will not do the configuration here.

public void configEngine(Engine me) {
    me.addSharedFunction("/view/common/layout.html");
    me.addSharedFunction("/view/common/paginate.html");
    me.addSharedFunction("/view/admin/common/layout.html");
}
Copy the code

configPlugin

Here is to configure the JFinal Plugin, that is, some plug-in information, my code is as follows:

public void configPlugin(Plugins me) {
    DruidPlugin dp = new DruidPlugin(p.get("jdbcUrl"), p.get("user"), p.get("password").trim());
    me.add(dp);

    ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
    arp.addMapping("user", User.class);
    me.add(arp);
}
Copy the code

My configuration is simple, with Druid’s database connection pool plug-in and ActiveRecord’s database access plug-in.

What makes me feel a little silly is that if I want to add a mapping to the ActiveRecord database access, I need to manually add code here, such as arp.addMapping(“aaa”, aaA.class); To return to the above problem, publishing between different environments requires manual modification of the system here. Small projects can also be managed manually, and large projects can become a nightmare here.

configInterceptor

This method is used to configure global interceptors, which fall into two categories: the control layer and the business layer. My sample code looks like this:

public void configInterceptor(Interceptors me) {
    me.add(new AuthInterceptor());
    me.addGlobalActionInterceptor(new ActionInterceptor());
    me.addGlobalServiceInterceptor(new ServiceInterceptor());
}
Copy the code

Here is me. The add (…). With me. AddGlobalActionInterceptor (…). The two methods are completely equivalent in that they are both interceptors configured to intercept action methods in all controllers. And me. AddGlobalServiceInterceptor (…). The configured interceptor intercepts all public methods of the business layer.

There is nothing to say about interceptors, this configuration feels exactly the same as in SpringBoot.

configHandler

This method is used to configure JFinal’s Handler, which can take over all Web requests and have complete control over the application.

This method is a high level extension method, I just want to write a simple CRUD operation, there is no need, here is an excerpt from the official Demo:

public void configHandler(Handlers me) {
    me.add(new ResourceHandler());
}
Copy the code

The configuration file

I looked at the official configuration file, the end was TXT, which made me start to doubt life at first glance, why the configuration file to choose TXT format, but the configuration format is exactly the same as the property file, is it to show personality, this makes me deeply suspicious.

In the previous DemoConfig class, it was possible to retrieve the contents of the configuration file directly using Prop:

static Prop p;

/** * PropKit.useFirstFound(...) Use the first configuration file found from left to right * of the parameters to find the configuration from left to right. If found, the configuration is loaded immediately and returned immediately. Subsequent configurations are ignored */
static void loadConfig(a) {
    if (p == null) {
        p = PropKit.useFirstFound("demo-config-pro.txt"."demo-config-dev.txt"); }}Copy the code

Although the concept of environment configuration is introduced in the configuration file, it is still a little rough. Many contents that need to be configured cannot be configured, and only limited contents such as database and cache service can be configured temporarily.

The Model configuration

To be honest, I was shocked when I saw the use of Model at the beginning. I didn’t expect it to be so simple:

public class User extends Model<User> {}Copy the code

Just like that, ok, what all need not write inside, a complete reversal of the cognitive, before I do this framework can dynamically to the database to find fields, the problem of not smart not smart, if two people together to develop the same project, I light on all don’t know the code Model of attribute has what inside, must want to watch to the database, This is going to freak you out.

JFinal provides a code generator, which is automatically generated based on the database table. Instead of looking at the generated code, take a look at the code of this automatic generator:

public static void main(String[] args) {
    // The package name used by base model
    String baseModelPackageName = "com.geekdigging.demo.model.base";
    // base model File saving path
    String baseModelOutputDir = PathKit.getWebRootPath() + "/src/main/java/com/geekdigging/demo/model/base";
    // Package name used by model (default package name used by MappingKit)
    String modelPackageName = "com.geekdigging.demo.model";
    // Save path of model file (default path of MappingKit and DataDictionary file)
    String modelOutputDir = baseModelOutputDir + "/..";
    // Create a generator
    Generator generator = new Generator(getDataSource(), baseModelPackageName, baseModelOutputDir, modelPackageName, modelOutputDir);
    // Configure whether to generate remarks
    generator.setGenerateRemarks(true);
    // Set the database dialect
    generator.setDialect(new MysqlDialect());
    // Sets whether chained setter methods are generated
    generator.setGenerateChainSetter(false);
    // Add table names that do not need to be generated
    generator.addExcludedTable("adv"."data"."rate"."douban2019");
    // Set whether to generate DAO objects in Model
    generator.setGenerateDaoInModel(false);
    // Sets whether to generate dictionary files
    generator.setGenerateDataDictionary(false);
    // Set the table name prefix to be removed for modelName generation. For example, if the table name is "OSC_user", the model name generated after removing the prefix "OSC_" is "User" instead of OscUser
    generator.setRemovedTableNamePrefixes("t_");
    / / generated
    generator.generate();
}
Copy the code

See this code my heart is cold, incredibly is the entire database to do the scan, fortunately MySQL, open source free, if it is Oracle, a project needs a database or a database cluster, this is too rich.

This code also provides a method to exclude unnecessary table names addExcludedTable(), which is not useful. An Oracle cluster may have N items running together with hundreds or thousands of tables. AddExcludedTable () will take a day or two to copy the name of the table.

Database CRUD operations

JFinal integrates CRUD operations on the data into the Model. I won’t comment on how this works, but take a look at a sample Service class I wrote:

public class UserService {
    private static final User dao = new User().dao();
    // paging query
    public Page<User> userPage(a) {
        return dao.paginate(1.10."select *"."from user where age > ?".18);
    }
    public User findById(String id) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.findById()>>>>>>>>>>>>>>>>>>>>>>>>>");
        return dao.findById(id);
    }
    public void save(User user) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.save()>>>>>>>>>>>>>>>>>>>>>>>>>");
        user.save();
    }
    public void update(User user) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.update()>>>>>>>>>>>>>>>>>>>>>>>>>");
        user.update();
    }
    public void deleteById(String id) {
        System.out.println(">>>>>>>>>>>>>>>>UserService.deleteById()>>>>>>>>>>>>>>>>>>>>>>>>>"); dao.deleteById(id); }}Copy the code

Select * from user where age >; select * from user where age >; Hibernate HQL, there is no secret between the two.

Other normal CRUD operations are written normally without any slots.

Controller

Code first, code Lao:

public class UserController extends Controller {

    @Inject
    UserService service;

    public void findById(a) {
        renderJson(service.findById("1"));
    }

    public void save(a) {
        User user = new User();
        user.set("id"."2");
        user.set("create_date".new Date());
        user.set("name"."Little red");
        user.set("age".24);
        service.save(user);
        renderNull();
    }

    public void update(a) {
        User user = new User();
        user.set("id"."2");
        user.set("create_date".new Date());
        user.set("name"."Little red");
        user.set("age".19);
        service.update(user);
        renderNull();
    }

    public void deleteById(a) {
        service.deleteById(getPara("id")); renderNull(); }}Copy the code

First, the Service uses @inject, which is not a big deal, just like @Autowaire in Spring.

All of the actual methods in this class return void, and the content is controlled by Render (), which can return JSON or page view, but it’s not a problem if it’s slightly uncomfortable.

But then I get a little bit more comfortable with this question. It doesn’t feel like a problem, it feels like a bug. There are only two ways to get parameters:

One is the getPara() family of methods, which can only fetch the data submitted by the form, basically similar to request.getParameter() in Spring.

The other is getModel/getBean. First, these two methods accept arguments submitted through the form, and second, they must be converted to a Model class.

I just want to know how to accept parameters if the request type is application/ JSON instead of form submission. I went through the document several times but couldn’t find the request object I wanted.

Maybe I just haven’t found a mature framework that doesn’t support this common application/ JSON data submission approach. It’s not possible.

Also, the getModel/getBean method must be converted directly to the Model class, which is sometimes not a good thing. If the input parameter format of the current interface is complex, this Model can be difficult to construct. In particular, sometimes only a small amount of data needs to be retrieved for parsing, and there is no need to parse the entire request data.

summary

JFinal as a whole does what a WEB + ORM framework is supposed to do with a simple CRUD operation, but some things are not as good, of course, compared to SpringBoot.

If it is used to do some small things feel still can be worth trying, if it is to do some enterprise applications, it is a little stretched.

However, this project came out relatively early. It has been 8 years since 2012. If it is compared with the framework of SpringMCV + Spring + ORM in that year, I think I will definitely choose JFinal.

If it is compared with the current SpringBoot, I think I still prefer To choose SpringBoot, one is familiar with, the other is JFinal a lot of places, in order to facilitate the use of developers, quite a lot of code is packaged, this practice can not be said to be bad, For beginners, it is certainly good, the document is simple, basic half a day to a day can start to work, but for some old drivers, it will make people feel tied, this can not do that can not do.

I have submitted my own sample code and the official Demo together to the code warehouse. If you need it, you can reply “JFinal” to get it.