本文中主要介紹一個(gè)集成了SpringDataJPA和FreeMarker的示例。
1、簡單介紹
1.1 SpringDataJPA
1.1.1 jdbc介紹
一般來說,數(shù)據(jù)存儲(chǔ)在關(guān)系型數(shù)據(jù)庫,而java語言直接操作的是對象。Java訪問數(shù)據(jù)庫,一般來來說,可能需要寫jdbc連接的代碼。
JDBC(Java Data Base Connectivity,java數(shù)據(jù)庫連接)是一種用于執(zhí)行SQL語句的Java API,可以為多種關(guān)系數(shù)據(jù)庫提供統(tǒng)一訪問,它由一組用Java語言編寫的類和接口組成。JDBC提供了一種基準(zhǔn),據(jù)此可以構(gòu)建更高級的工具和接口,使數(shù)據(jù)庫開發(fā)人員能夠編寫數(shù)據(jù)庫應(yīng)用程序。
我們安裝好數(shù)據(jù)庫之后,我們的應(yīng)用程序也是不能直接使用數(shù)據(jù)庫的,必須要通過相應(yīng)的數(shù)據(jù)庫驅(qū)動(dòng)程序,通過驅(qū)動(dòng)程序去和數(shù)據(jù)庫打交道。其實(shí)也就是數(shù)據(jù)庫廠商的JDBC接口實(shí)現(xiàn),即對Connection等接口的實(shí)現(xiàn)類的jar文件。

從這個(gè)圖中,可以看出jdbc也是一個(gè)非常優(yōu)雅的設(shè)計(jì)實(shí)現(xiàn),通過它,應(yīng)用程序員無需關(guān)注不同數(shù)據(jù)庫廠商的實(shí)現(xiàn)差異。
1.1.2 ORM介紹
JDBC連接相關(guān)的代碼,對于應(yīng)用程序原來說,還是比較繁瑣的。這里出現(xiàn)了一列的ORM(Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping,對象關(guān)系映射)框架:
- MyBatis:MyBatis 本是 Apache 的一個(gè)開源項(xiàng)目 iBatis,2010 年這個(gè)項(xiàng)目由 Apache Software Foundation 遷移到了 Google Code,并且改名為 MyBatis,其著力于 POJO 與 SQL 之間的映射關(guān)系,可以進(jìn)行更為細(xì)致的 SQL,使用起來十分靈活、上手簡單、容易掌握,所以深受開發(fā)者的喜歡,目前市場占有率最高,比較適合互聯(lián)應(yīng)用公司的 API 場景;缺點(diǎn)就是工作量比較大,需要各種配置文件的配置和 SQL 語句。
- Hibernate:Hibernate 是一個(gè)開放源代碼的對象關(guān)系映射框架,它對 JDBC 進(jìn)行了非常輕量級的對象封裝,使得 Java 程序員可以隨心所欲的使用對象編程思維來操縱數(shù)據(jù)庫,并且對象有自己的生命周期,著力點(diǎn)對象與對象之間關(guān)系,有自己的 HQL 查詢語言,所以數(shù)據(jù)庫移植性很好。Hibernate 是完備的 ORM 框架,是符合 JPA 規(guī)范的,有自己的緩存機(jī)制,上手來說比較難,比較適合企業(yè)級的應(yīng)用系統(tǒng)開發(fā)。
- Spring Data JPA:可以理解為 JPA 規(guī)范的再次封裝抽象,底層還是使用了 Hibernate 的 JPA 技術(shù)實(shí)現(xiàn),引用 JPQL(Java Persistence Query Language)查詢語言,屬于 Spring 的整個(gè)生態(tài)體系的一部分。由于 Spring Boot 和 Spring Cloud 在市場上的流行,Spring Data JPA 也逐漸進(jìn)入大家的視野,他們有機(jī)的整體,使用起來比較方便,加快了開發(fā)的效率,使開發(fā)者不需要關(guān)系和配置更多的東西,完全可以沉浸在 Spring 的完整生態(tài)標(biāo)準(zhǔn)的實(shí)現(xiàn)下,上手簡單、開發(fā)效率高,又對對象的支持比較好,又有很大的靈活性,市場的認(rèn)可度越來越高。
- OpenJPA :是 Apache 組織提供的開源項(xiàng)目,它實(shí)現(xiàn)了 EJB 3.0 中的 JPA 標(biāo)準(zhǔn),為開發(fā)者提供功能強(qiáng)大、使用簡單的持久化數(shù)據(jù)管理框架,但功能、性能、普及性等方面更加需要加大力度,所以用的人不人不是特別多。
- QueryDSL:QueryDSL 可以在任何支持的 ORM 框架或者 SQL 平臺(tái)上以一種通用的 API 方式來構(gòu)建查詢,目前 QueryDSL 支持的平臺(tái)包括 JPA、JDO、SQL、Java Collections、RDF、Lucene、Hibernate Search,同時(shí) Spring Data JPA 也對 QueryDSL 做了很好的支持。
1.1.3 JPA介紹
JPA(Java Persistence API)中文名 Java 持久層 API,是 JDK 5.0 注解或 XML 描述對象-關(guān)系表的映射關(guān)系,并將運(yùn)行期的實(shí)體對象持久化到數(shù)據(jù)庫中。
Sun 引入新的 JPA ORM 規(guī)范出于兩個(gè)原因:其一,簡化現(xiàn)有 Java EE 和 Java SE 應(yīng)用開發(fā)工作;其二,Sun 希望整合 ORM 技術(shù),實(shí)現(xiàn)天下歸一。
JPA 包括以下三方面的內(nèi)容:
- 一套 API 標(biāo)準(zhǔn),在 javax.persistence 的包下面,用來操作實(shí)體對象,執(zhí)行 CRUD 操作,框架在后臺(tái)替代我們完成所有的事情,開發(fā)者從繁瑣的 JDBC 和 SQL 代碼中解脫出來。
- 面向?qū)ο蟮牟樵冋Z言:Java Persistence Query Language(JPQL),這是持久化操作中很重要的一個(gè)方面,通過面向?qū)ο蠖敲嫦驍?shù)據(jù)庫的查詢語言查詢數(shù)據(jù),避免程序的 SQL 語句緊密耦合。
- ORM(Object/Relational Metadata)元數(shù)據(jù)的映射,JPA 支持 XML 和 JDK 5.0 注解兩種元數(shù)據(jù)的形式,元數(shù)據(jù)描述對象和表之間的映射關(guān)系,框架據(jù)此將實(shí)體對象持久化到數(shù)據(jù)庫表中。
JPA 的宗旨是為 POJO 提供持久化標(biāo)準(zhǔn)規(guī)范,由此可見,經(jīng)過這幾年的實(shí)踐探索,能夠脫離容器獨(dú)立運(yùn)行,方便開發(fā)和測試的理念已經(jīng)深入人心了。Hibernate 3.2+、TopLink 10.1.3 以及 OpenJPA、QueryDSL 都提供了 JPA 的實(shí)現(xiàn),以及最后的 Spring 的整合 Spring Data JPA。目前互聯(lián)網(wǎng)公司和傳統(tǒng)公司大量使用了 JPA 的開發(fā)標(biāo)準(zhǔn)規(guī)范。

1.2 FreeMarker
FreeMarker是一款模板引擎:即一種基于模板和要改變的數(shù)據(jù),并用來生成輸出文本(HTML網(wǎng)頁,電子郵件,配置文件,源代碼等)的通用工具。它不是面向最終用戶的,而是一個(gè)Java類庫,是一款程序員可以嵌入他們所開發(fā)產(chǎn)品的組件。
模板編寫為FreeMarker Template Language(FTL)。它是簡單的,專用的語言, 不是像PHP那樣成熟的編程語言。那就意味著要準(zhǔn)備數(shù)據(jù)在真實(shí)編程語言中來顯示,比如數(shù)據(jù)庫查詢和業(yè)務(wù)運(yùn)算,之后模板顯示已經(jīng)準(zhǔn)備好的數(shù)據(jù)。在模板中,你可以專注于如何展現(xiàn)數(shù)據(jù), 而在模板之外可以專注于要展示什么數(shù)據(jù)。

這種方式通常被稱為MVC(模型視圖控制器)模式,對于動(dòng)態(tài)網(wǎng)頁來說,是一種特別流行的模式。它幫助從開發(fā)人員(Java 程序員)中分離出網(wǎng)頁設(shè)計(jì)師(HTML設(shè)計(jì)師)。設(shè)計(jì)師無需面對模板中的復(fù)雜邏輯,在沒有程序員來修改或重新編譯代碼時(shí),也可以修改頁面的樣式。
而FreeMarker最初的設(shè)計(jì),是被用來在MVC模式的Web開發(fā)框架中生成HTML頁面的,它沒有被綁定到Servlet或HTML或任意Web相關(guān)的東西上。它也可以用于非Web應(yīng)用環(huán)境中。
FreeMarker是免費(fèi)的,基于Apache許可證2.0版本發(fā)布。
2、代碼實(shí)現(xiàn)
2.1 創(chuàng)建基本的工程
pom.xml文件,中引入了jpa、freemarker和springboot等相關(guān)的依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy.springboot</groupId>
<artifactId>FreemarkerTrial</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot JPA的起步依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<!-- MySQL連接驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
</dependencies>
</project>
配置文件中配置了freemarker模板信息、數(shù)據(jù)庫連接信息等,application.properties:
spring.datasource.username=root
spring.datasource.password=lfqylfqy
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#JPA Configuration:
spring.jpa.database=MySQL
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
#設(shè)定ftl文件路徑
spring.freemarker.template-loader-path=classpath:/templates
寫一個(gè)springboot的啟動(dòng)類com/lfqy/freemarker/SpringBootTrialApplication.java:
package com.lfqy.freemarker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootTrialApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTrialApplication.class);
}
}
寫一個(gè)測試的Controller類com/lfqy/freemarker/controller/TrialController.java:
package com.lfqy.freemarker.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TrialController {
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "springboot is ok!";
}
}
實(shí)際上,到這里,工程就可以啟動(dòng)了。訪問/hello返回springboot is ok!。
2.2 數(shù)據(jù)庫相關(guān)的內(nèi)容
2.2.1 準(zhǔn)備測試數(shù)據(jù)
mysql數(shù)據(jù)庫中執(zhí)行如下腳本,準(zhǔn)備測試數(shù)據(jù)。創(chuàng)建一個(gè)user表,插入兩條測試數(shù)據(jù):
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '張三');
INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四');
COMMIT;
2.2.2 pojo、DAO和service
寫一個(gè)pojo類com/lfqy/freemarker/pojo/User.java:
package com.lfqy.freemarker.pojo;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* Created by chengxia on 2022/4/14.
*/
@Entity
public class User {
// 主鍵
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 用戶名
private String username;
// 密碼
private String password;
// 姓名
private String name;
//setter和getter方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
DAO類直接繼承的jpa的接口,com/lfqy/freemarker/dao/UserDao.java:
package com.lfqy.freemarker.dao;
import com.lfqy.freemarker.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Created by chengxia on 2022/4/14.
*/
public interface UserDao extends JpaRepository<User, Long> {
}
service的接口類,com/lfqy/freemarker/service/UserService.java:
package com.lfqy.freemarker.service;
import com.lfqy.freemarker.pojo.User;
import java.util.Optional;
/**
* Created by chengxia on 2022/4/14.
*/
public interface UserService {
Optional<User> getUserById(long id);
}
service的接口實(shí)現(xiàn)類,com/lfqy/freemarker/service/impl/UserServiceImpl.java:
package com.lfqy.freemarker.service.impl;
import com.lfqy.freemarker.dao.UserDao;
import com.lfqy.freemarker.pojo.User;
import com.lfqy.freemarker.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
/**
* Created by chengxia on 2022/4/14.
*/
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public Optional<User> getUserById(long id) {
return userDao.findById(id);
}
}
在這個(gè)類中,直接調(diào)用了jpa的現(xiàn)成方法查詢數(shù)據(jù)庫。
JpaRepository查詢方法解析流程說明:
Spring Data JPA框架在進(jìn)行方法名解析時(shí),會(huì)先把方法名多余的前綴截取掉
比如find、findBy、read、readBy、get、getBy,然后對剩下部分進(jìn)行解析。這里的findById就是根據(jù)ID屬性進(jìn)行查找的意思。如下列出了一些例子:
| Keyword | Sample | JPQL snippet |
|---|---|---|
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
| Is,Equals | findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
| LessThan | findByAgeLessThan | … where x.age < ?1 |
| LessThanEqual | findByAgeLessThanEqual | … where x.age ? ?1 |
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
| After | findByStartDateAfter | … where x.startDate > ?1 |
| Before | findByStartDateBefore | … where x.startDate < ?1 |
| IsNull | findByAgeIsNull | … where x.age is null |
| IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
| Like | findByFirstnameLike | … where x.firstname like ?1 |
| NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
| Not | findByLastnameNot | … where x.lastname <> ?1 |
| In | findByAgeIn(Collection ages) | … where x.age in ?1 |
| NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
| TRUE | findByActiveTrue() | … where x.active = true |
| FALSE | findByActiveFalse() | … where x.active = false |
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
2.3 Controller和視圖
寫一個(gè)Controller,其中包含及直接將查詢結(jié)果返回的方法,也包含一個(gè)將查詢出的對象信息通過模板渲染成html例子的方法。
com/lfqy/freemarker/controller/UserController.java:
package com.lfqy.freemarker.controller;
import com.lfqy.freemarker.pojo.User;
import com.lfqy.freemarker.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
/**
* Created by chengxia on 2022/4/14.
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/user/{id}")
private User getUserById(@PathVariable long id) {
User user = userService.getUserById(id).get();
return user;
}
@RequestMapping("/show/{idStr}")
@ResponseBody
private ModelAndView showUserById(@PathVariable String idStr) {
long id = Long.parseLong(idStr);
User user = userService.getUserById(id).get();
ModelAndView mv = new ModelAndView();
mv.addObject("user",user);
mv.setViewName("userInfo");
return mv;
}
}
這里的showUserById方法,返回的是一個(gè)名為userInfo的模板,這個(gè)模板是一個(gè)以userInfo命名的ftl文件。
templates/userInfo.ftl:
<html>
<head>
<title>User Info</title>
</head>
<body>
用戶列表:<br>
<table border="1">
<tr>
<th>id</th>
<th>username</th>
<th>password</th>
<th>name</th>
</tr>
<#--<#list userList as user>-->
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.password}</td>
<td>${user.name}</td>
</tr>
<#--</#list>-->
</table>
</body>
</html>
3、運(yùn)行效果和提示
到這里,全部的編碼就完成了。工程結(jié)構(gòu)如下:

運(yùn)行
com.lfqy.freemarker.SpringBootTrialApplication,可以啟動(dòng)工程。訪問
http://localhost:8080/hello:
訪問http://localhost:8080/user/1:

訪問http://localhost:8080/show/1:

這里在之前調(diào)試的時(shí)候,訪問http://localhost:8080/show/1,對應(yīng)的Controller方法總會(huì)執(zhí)行兩遍,查了好久,發(fā)現(xiàn)是由于沒有在pom文件中引入freemarker依賴,導(dǎo)致方法中返回的mv不會(huì)被認(rèn)為是一個(gè)模板,而是會(huì)把模板名放到參數(shù)的位置,重新執(zhí)行一般Controller方法。添加freemarker的pom依賴后,問題得以正常解決。
4、freemarker模板的嵌套示例
新增一個(gè)測試模板嵌套的Controller,com/lfqy/freemarker/controller/FtlIncludeController.java:
package com.lfqy.freemarker.controller;
import com.lfqy.freemarker.pojo.User;
import com.lfqy.freemarker.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* Created by chengxia on 2022/4/14.
*/
@RestController
public class FtlIncludeController {
@RequestMapping("/index")
private ModelAndView index() {
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
}
新增一個(gè)模板文件,其中嵌套了好多子模板。
templates/index.ftl:
<html>
<head>
<title>FreeMarker include demo page</title>
<#include "pubjs.ftl">
<script>
function foo() {
alert('This is alert of function foo!');
}
<#include "jsfunc.ftl">
function jquerytest() {
//alert($('#testp'));
$('#testp').css("background","yellow");
}
</script>
</head>
<body>
<#include "top.ftl">
<br/>
<p id="testp">top middle line</p>
<br/>
<#include "top.ftl">
<br/>
<button onclick="foo()">ExecuteFoo</button>
<button onclick="bar()">ExecuteBar</button>
<button onclick="jquerytest()">jquerytest1</button>
<button onclick="$('#testp').css('background','blue');">jquerytest2</button>
<p>middle bottom line</p>
<br/>
<#include "bottom.ftl">
</body>
</html>
templates/top.ftl:
<h2>This is top area.</h2>
templates/middle.ftl:
<h2>This is middle area.</h2>
templates/bottomo.ftl:
<h2>This is bottom area.</h2>
templates/jsfunc.ftl:
function bar() {
alert('This is alert of function bar!');
}
templates/pubjs.ftl:
<script src="jquery1.10.2.min.js"></script>
完成之后工程目錄如下:

訪問http://localhost:8080/index:

點(diǎn)擊頁面按鈕,運(yùn)行都符合代碼邏輯預(yù)期。