Spring-reference 项目为 Spring 整合 MyBatis + Spring MVC,以及 Java Web 方向上多种常用的技术的演练,作为参考和交流学习的资料。另外,也是为了更好地掌握 Spring Boot 打下基础。该项目注释完备,代码规范,只做技术演练,没有过多复杂的业务逻辑,适合作为新手学习的参考。项目地址:Shouheng88/Spring=references。
另外,笔者整理了 Spring IOC, AOP, MVC, 事务管理以及 Servlet 和 JSP 相关的内容。主要是整理了一些比较重点的内容作为开发的参考,以下是文章的链接地址。在这篇文章中,我们不会讨论如何这些框架的基本的使用,而是对整合它们到框架当中提出解决方案:
在开发的时候,我们通常有一些基础的类放在一个单独的模块中。在 Gradle 中,我们可以将其放置在各个模块中。然后,我们将其通过在 setting.gradle 中配置模块的文件路径。在 maven 当中,我们可以使用类似的方式来解决同样的问题。在示例项目当中,我们将代码放进两个模块当中:
在 maven 当中,我们可以按照下面这样来配置模块的目录:
首先,项目当中的 pom.xml 共有三个,两个子模块各占一个,然后一个公共的父 pom.xml。因此,项目当中的 pom.xml 文件结构如下:
|-----
|----common
|----pom.xml
|----service
|----pom.xml
|----pom.xml
在三个 pom.xml 文件中,我么需要作如下的配置:
<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.0modelVersion>
<groupId>spring-referencesgroupId>
<artifactId>spring-referencesartifactId>
<version>1.0version>
<packaging>pompackaging>
<name>Spring-referencesname>
<url>https://github.com/Shouheng88/Spring-referencesurl>
<modules>
<module>servicemodule>
<module>commonmodule>
modules>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>${junit.version}version>
<scope>testscope>
dependency>
dependencies>
dependencyManagement>
<build>
<finalName>spring-referencesfinalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>8source>
<target>8target>
configuration>
plugin>
plugins>
pluginManagement>
build>
project>
<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.0modelVersion>
<artifactId>serviceartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>warpackaging>
<name>Servicename>
<url>https://github.com/Shouheng88/Spring-referencesurl>
<parent>
<groupId>spring-referencesgroupId>
<artifactId>spring-referencesartifactId>
<version>1.0version>
parent>
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<scope>testscope>
dependency>
dependencies>
project>
对于 common 模块的 pom 可以采用类似的处理方式,这里不再进行说明。
在当前的示例项目当中,我们通过 Spring 提供的一些框架来实现一些常用的功能:
为了实现 json 格式的接口,那么要考虑的问题又包括:
下面我们来整理下这些东西是如何进行处理的。
对于后端自身而言,通常我们会根据业务将数据分成几种类型,包括
为了简化我们的代码,我们还可以使用一个叫做 lombok 的插件来简化我们的数据结构。当然,lombok 的作用无非就是简化 setter/getter 和一些构造方法。这能让我们的数据实体看上去更加清爽。
为了使用 lombok 我们只需要做两处配置就可以了。
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok}version>
dependency>
首先呢,一个基础的 PO 是必不可少的。我们通常使用它们来定义所有的 PO 都应该均有的基础字段,比如 id, 创建时间, 最后的更新时间, 备注和乐观锁版本信息等。因此,一个基础的 PO 类应该是下面这样:
@Data
public abstract class AbstractPO implements Serializable {
private static final long serialVersionUID = 6982434571375510313L;
@Id
@Column(name = "id")
private Long id;
@Column(name = "created_time")
private Date createdTime;
@Column(name = "updated_time")
private Date updatedTime;
@Column(name = "remark")
private String remark;
@Version
@Column(name = "lock_version")
private Integer lockVersion;
}
VO 的设计包含两部分内容。首先,我们需要定义对应于 PO 的 VO 类。这种类用来定义返回给前端的数据结构。它并不一定与 PO 类的结构完全相同,而是根据接口的需要选择性地下发部分字段或者新增一些字段。
对 VO,我们可以定义一个基础的抽象类如下。也就是定义了一些对应于 PO 的字段信息:
@Data
public abstract class AbstractVO implements Serializable {
private static final long serialVersionUID = 1;
private Long id;
private Date createdTime;
private Date updatedTime;
private String remark;
private Integer lockVersion;
}
除了 VO 我们还要定义 PackVo. 它用来包装 VO 的信息,另外提供一些用来返回给前端的冗余字段信息,以及服务端返回的错误信息封装等。对于 PackVo,我们也可以定义一个基础的抽象类 AbstractPackVo:
@Data
public abstract class AbstractPackVo implements Serializable {
private static final long serialVersionUID = -2119661016457733317L;
private Boolean success = true;
private List<ClientMessage> messages;
private Long udf1;
private String udf2;
private String udf3;
private String udf4;
private String udf5;
private String udf6;
}
这里的 AbstractPackVo 是一个基础的抽象类。针对具体要返回的业务数据结构,我们需要定义对应的具体的类,并继承 AbstractPackVo:
@Data
public class PackTaskVo extends AbstractPackVo {
private static final long serialVersionUID = 1L;
private TaskVo vo;
private List<TaskVo> voList;
}
ClientMessage 就是用来返回给前端的错误信息的包装类。我们可以按照下面这样的方式来进行定义:
@Data
public class ClientMessage implements Serializable {
private static final long serialVersionUID = -1L;
private Long id;
private String code;
private String message;
private String messageCN;
}
这里的 code 用来表示错误信息的代码,我们可以统一将错误信息定义在 properties 文件中,并将其置于项目的 resources 目录下面。
#测试消息
E000000000000000=测试消息{0} {1}
#乐观锁异常
E000000000000001=该记录已经被其他用户修改
#空指针异常
E000000000000002=NullPointerException
#系统错误
E000000000000003=SystemErrorException
#DAO错误
E000000000000004=DAOException
然后,在我们的项目中,我们可以通过单例的工具类来从 properties 文件中读取错误信息:
public class ErrorDispUtils {
private static Logger logger = LoggerFactory.getLogger(ErrorDispUtils.class);
private static final String CONFIG_FILE = "error-disp.properties";
private static ErrorDispUtils instance = new ErrorDispUtils();
private static Configuration config;
public static ErrorDispUtils getInstance() {
return instance;
}
private ErrorDispUtils() {
try {
config = new PropertiesConfiguration(CONFIG_FILE);
} catch (Exception e) {
logger.error("ErrorDispUtils initialize error" ,e);
}
}
// 用来读取指定 code 的字符串
public String getValue(String key) {
return config.getString(key);
}
}
对于 ClientMessage,我们可以在项目中通过 AOP 来进行拦截,然后做一个统一的处理。
SO 是前端提交给客户端的 json 的数据结构,对于它我们也需要做一个简单的封装。这里我们也提供一个基础的类 SearchObject:
public class SearchObject implements Pageable, Sortable, Serializable {
private static final long serialVersionUID = 4009650343975989289L;
private int currentPage;
private int pageSize;
private List<Sort> sorts = new LinkedList<>();
// ... setters and getters
}
这里的 SearchObject 实现了 Pageable 和 Sortable 两个自定义接口。它们分别用来进行分页和指定用来排序的字段:
public interface Pageable {
int getCurrentPage();
void setCurrentPage(int currentPage);
int getPageSize();
void setPageSize(int pageSize);
}
public interface Sortable {
List<Sort> getSorts();
void addSort(Sort sort);
}
public class Sort implements Serializable {
private static final long serialVersionUID = 7739709965769082011L;
private String sortKey;
private String sortDir;
}
这里的 Sort 定义了两个字符串类型的字段,分别表示排序的字段以及排序的方向。
上面我们定义的数据结构是用在服务端自身的,比如 Service 返回数据。但是,它还无法直接用于前后端的交互。因为前端可能需要一些额外的参数代表设备信息,返回给后端的数据也可能包含一些错误信息等,也就是 ClientMessage 中的信息。因此,我们需要设计新的数据结构。下面 BusinessRequest 和 BusinessResponse 就分别用作将前端数据传递给后端以及后端返回数据给前端:
@Data
public class BusinessRequest<T> {
private Integer clientVersion;
private Date clientTime;
private String phoneNumber;
private String iMEI;
private String iMSI;
private String deviceID;
private Long userID;
private String token;
private String requestType;
private T requestData;
private List<T> requestDataList;
}
@Data
public class BusinessResponse<T> {
private Boolean isSuccess;
private Long serverFlag;
private String serverMessage;
private T responseData;
private List<T> responseDataList;
private Long udf1;
}
以上只是我们定义前后端交互的数据结构的一种方式,实际上我们可以有很多不同的方式来定义交互的格式。只是,在项目开发之前,这种基础的交互格式需要前后端进行沟通之后来确定。上面的开发的方式也需要按照我们指定的格式来定义才能正确地把我们返回的数据从 Json 映射成基础的 java 类。
除了前后端的问题,定义数据结构还将影响我们的开发。比如我们可以在 MyBatis 的文件中定义一个 sql 代码片段来在我们的项目中使用 Sort 字段:
<sql id="Order_By_Clause">
<if test="sorts != null and sorts.size > 0">
ORDER BY
<if test="item.sortKey != null and item.sortKey != ''">t.${item.sortKey}if>
<if test="item.sortDir != null and item.sortDir != ''">${item.sortDir}if>
foreach>
if>
sql>
很显然,这得益于我们以上定义的那一套数据结构规范。而这套规范一旦最终确定下来,我们可以做更多的操作来简化我们的开发。(显然,如果这样做了,一旦数据结构改动,调整的成本也是很高的!)
不过,这种数据交互的格式,我们还是尽量采用一种可靠的、比较成熟都是解决方案,免得在开发的过程中因为数据结构的问题延缓开发进度。而上面的这种格式本身就是一种比较成熟的解决方案了。
在使用 Spring 开发的过程中几乎必选的三个核心的模块:IoC, AOP 和 MVC. 这里我们主要说明的是 Spring 的这三个模块的整合。
首先我们需要在项目当中引用 Spring 所需的各种依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
dependency>
这里我们引用的比较多,主要包括:Spring 核心库、Spring 容器、Spring 事务管理框架以及 MVC 框架。
然后,我们需要在项目当中整合 Spring 的各个库。
首先是 Spring MVC. 我们需要在项目的 web.xml 中添加如下的配置:
<servlet>
<servlet-name>spring-mvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring/spring-*.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>spring-mvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
这里我们指定 Servlet 的信息,这里前面使用了 Spring MVC 的 Servlet 来对请求做分发。然后使用 init-param 标签指定了 Spring 上下文的配置文件的位置。本质上这里作用的原理是通过解析 xml 文件来读取配置信息并生成项目中的类。比如,这里会创建一个 DispatcherServlet 类,然后这里的 init-param 中的键值对会被通过 setter 方法注入到生成的 DispatcherServlet 实例中。本质上这些逻辑都是在 Servlet 即 Tomcat 或者 Netty 等当中完成的。
这里的 servlet 和 servlet-mapping 标签是一一对应的,它们通过 servlet-name 来实现匹配关系。这里的 servlet-mapping 标签用来指定名为 servlet-mvc 的 servlet 能够处理的 url.
上面,我们使用了通配符来指定多个配置文件,它们的规则是名称以 spring-
开头的 xml 文件,并且都处于 resources 的 spring 目录下面。这样做是因为我们希望把项目当中的 Spring 的配置文件按照它的功能分配到不同的配置文件当中去。比如,在示例项目中,我们的 Spring 的配置文件如下:
spring-dao.xml
: 用来配置 DAO 相关的各种 Bean;spring-service.xml
:用来配置 Service 相关的各种 Bean;spring-shiro.xml
:用来配置 shiro 相关的 Bean 的信息;spring-web.xml
:用来配置 web 相关的 Bean 的信息。默认情况下,DispatcherServlet 会加载 WEB-INF/[DispatcherServlet的Servlet名字]-servlet.xml
下面的配置文件。根据上面的配置我们需要在当前项目的 WEB-INF 目录下面加入 spring-mvc-servlet.xml 文件。
另外需要注意的地方是,这里我们直接使用 Spring MVC 来分发所有类型的请求。具体是使用 Json 进行交互还是返回 Jsp 页面,我们在代码中使用 Spring 的 RequestMapping 注解来完成。
下面我们先看一下使用 Spring MVC 的时候需要做哪些配置。
首先让我们看下 Spring 的 web 相关的配置文件:
<beans xmlns=...>
<description>Spring Web 层的配置description>
<mvc:annotation-driven />
<context:component-scan base-package="me.shouheng.service.controller">
context:component-scan>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
bean>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="4194304" />
<property name="maxInMemorySize" value="4194304" />
bean>
beans>
上面的注解已经很完备了,这里我们再简单说明下各个配置项的作用:
本质上按照上面的配置,我们已经可以把前端传入的请求处理之后返回给后端了。我们可以使用下面的例子来进行简单的测试:
@Controller
@RequestMapping(path = {PATH_PREFIX})
public class TaskController {
private static final Logger logger = LoggerFactory.getLogger(TaskController.class);
static final String PATH_PREFIX = "/task";
private static final String LIST = "/all";
private static final String PAGE = "/page";
/**
* 测试用来发送 restful 类型的请求的接口
*
* @param taskSo 请求对象,Json 类型,放置在 body 中
* @return 返回对象
*/
@ResponseBody
@RequestMapping(value = LIST, method = RequestMethod.POST)
public PackTaskVo listAll(@RequestBody TaskSo taskSo) {
logger.info("----------- received : " + taskSo);
return new PackTaskVo();
}
/**
* 测试用来请求 jsp 页面的接口
*
* @return jsp 页面(名称),映射到 view/task.jsp
*/
@RequestMapping(value = PAGE, method = RequestMethod.GET)
public String testPage() {
logger.info("----------- requesting test page.");
return "task";
}
}
在上面的这个例子中,我们使用到了之前定义的数据结构。这里的 @Controller 注解表明这个类是一个控制器。这里的 @RequestMapping 注解用来指定路由的映射关系。
另外,上面我们使用到了 Looger,这是一个日志框架,我们稍后会说明如何集成日志框架。
关于数据源的问题我们几个问题需要关注:
对于数据库类型而言,业务开发的时候通常使用关系型数据库,进行缓存的时候会使用非关系型数据库比如 Redis 或者 MemCached. 对于关系型数据库,我们可以使用 MySQL 或者其他数据库。这里我们为了进行演示,使用两种关系型数据库。分别是 MySQL 对应于生成环境,H2 数据库对应于测试开发环境。因此这就需要我们引入 MySQL 和 H2 的数据库连接驱动的依赖:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql-connecotr}version>
dependency>
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<version>${h2.version}version>
dependency>
那么根据开发环境选择数据库的时候,我们有很多种配置方式。比如,我们可以在 MyBatis 中构建数据库连接的时候进行配置或者在 Spring 加载不同的配置文件的时候指定环境。但是,我们应该尽量使用一种配置方式,避免同时使用多种配置方式。这里我们使用 Spring 上下文来进行配置:
<context-param>
<param-name>spring.profiles.activeparam-name>
<param-value>devparam-value>
context-param>
<beans profile="dev">
<context:property-placeholder location="classpath*:configs/jdbc-dev.properties" />
beans>
<beans profile="test">
<context:property-placeholder location="classpath*:configs/jdbc-test.properties" />
beans>
<beans profile="prod">
<context:property-placeholder location="classpath*:configs/jdbc-prod.properties" />
beans>
具体数据库连接等信息被放置在各个配置环境文件当中。
对于数据库访问框架的问题,我们这里使用 MyBatis. 毕竟它当前属于主流的数据库访问框架,相对于 Herbinate 拓展性比较好。对于 MyBatis 的集成我们会在后面进行说明。
当程序在运行的过程中出现错误的时候,我们希望能够对错误进行统一的处理,然后将错误信息包装成 ClientMenssage 之后返回给客户端。在 Spring 当中,我们可以使用 AOP,通过切面来实现异常的统一处理。
这里我们使用如下的配置来实现对异常的处理:
<context:component-scan base-package="me.shouheng.service.*"/>
<bean id="serviceHandler" class="me.shouheng.service.common.aop.ServiceMethodInterceptor"/>
<aop:config>
<aop:pointcut id="servicePointcut" expression="within(me.shouheng.service.service.impl.*)"/>
<aop:advisor advice-ref="serviceHandler" id="serviceAdvisor" pointcut-ref="servicePointcut"/>
aop:config>
这里的第一点是启用注解自动扫描以发现项目中的 Service,然后定义一个拦截器 ServiceMethodInterceptor。这里使用的是 JDK 动态代理来实现对异常的控制。
在下面的 aop:config 标签当中我们定义了要拦截的 Service 的规则。也就是 impl 包下面的所有的类的所有方法。
那么下面我们来看下这个拦截器是如何定义的:
public class ServiceMethodInterceptor implements MethodInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ServiceMethodInterceptor.class);
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Method targetMethod = methodInvocation.getMethod();
Object ret;
try {
// 初始化数据库连接
SqlSessionHolder.initReuseSqlSession();
// 进行安全校验并触发方法
ret = checkSecurityAndInvokeBizMethod(methodInvocation);
// 提交事务
SqlSessionHolder.commitSession();
} catch (Exception e) {
logger.error("Error calling " + targetMethod.getName() + " : " + e);
// 事务回滚
SqlSessionHolder.rollbackSession();
// 包装异常信息之后将其返回给客户端
return this.createExceptionResult(methodInvocation, e);
} finally {
// 清空会话信息
SqlSessionHolder.clearSession();
}
return ret;
}
}
上面是我们的自定义拦截器。这里在执行拦截的方法之前会调用 SqlSessionHolder.initReuseSqlSession()
启动数据库连接会话。然后在 checkSecurityAndInvokeBizMethod()
方法中执行指定的方法。这里如果方法执行的过程中没有出现任何错误,那么我们就可以使用 SqlSessionHolder.commitSession()
提交事务。如果在执行方法的过程中出现了错误,那么我们就在上面的拦截器当中 catch,根据 Service 的返回格式,创建包含异常信息的返回结果。并进行事务回滚。
不过这里有一个问题就是,如果一个 Controller 调用了多个 Service. 当其中的一个出现问题的时候只能保证这个 Service 的方法返回了错误的信息。但这个 Controller 可能会继续调用其他的 Service 的方法。因此,这种拦截的逻辑最好以 Controller 的方法作为维度进行控制。
就像前面提到的,我们提供了两种整合 MyBatis 的方式。一种是对 MyBatis 的各个方法做了封装的方式。这种方式有固定的格式,对 Mapper 的命名以及其内部的方法的命名有严格的要求。这种整合方式配合我们的 Generator 可以降低我们开发的复杂度。另一种整合方式即使用 MyBatis 文件内部的规则来实现映射关系。
上面我们已经提到过 MyBatis 的数据源如何根据环境来进行配置。本质上上面提到的只是根据当期的开发环境在 xml 配置文件中引用不同的 properties 文件。下面我们来看下如何进行配置:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
这里其实我们使用的是阿里巴巴开源的 Druid 数据库连接池的数据源来实现的。其中的主要的内容是上面的几个占位符,这些也就是我们需要从 properties 文件中读取到的属性的值。比如,开发环境中我们可能指定这些值:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
jdbc.username=root
jdbc.password=qweasdzxc
Spring 本身已经为我们提供了一套事务管理机制。这里我们使用一套自定义的事务管理机制。上面说明 AOP 的时候我们也提到过事务回滚的和提交的内容,那就是我们用来实现事务的逻辑。这里我们主要说明下这里是如何基于 MyBatis 的事务进行管理的。
因为本质上我们之心 SQL 的时候都是使用 MyBatis 的 SqlSession 来完成的,而 SqlSession 又都是从 SqlSessionFactory 中获取到的,SqlSessionFactory 又是根据各个配置文件来创建的。因此,对于每个线程的会话,我们可以将其放置到线程的局部变量中缓存起来。然后需要执行 SQL 的时候从缓存中提取并使用即可。
因此,在进行数据库访问之前我们需要初始化 SqlSessionFactory 以获取 SqlSession;在执行完 SQL 之后再根据需要使用 SqlSession 的方法提交或者回滚。
这里我们使用类 SqlMapClientHolder 来初始化各个环境对应的 SqlSessionFactory. 然后在 SqlSessionHolder 中使用 SqlMapClientHolder 获取 SqlSessionFactory 并存取各个线程的单例的 SqlSession.
以上就是我们基于 MyBatis 的 SqlSession 进行事务管理的实现逻辑。
这种配置方式比较简单,我们可以先来说明下这种配置是如何实现的。首先,我们需要在 spring-dao.xml 配置文件中对 MyBatis 进行配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:ibatis-config.xml" />
<property name="typeAliasesPackage" value="me.shouheng.service.model" />
<property name="mapperLocations" value="classpath:mybatis/*.xml" />
<property name="typeHandlers">
<array>
<bean class="me.shouheng.common.dao.handler.DateTypeHandler"/>
array>
property>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="me.shouheng.service.dao" />
bean>
在上面的这段代码中,我们指定了数据源、MyBatis 配置文件的位置、entity 包的位置、Mapper 文件的位置以及一些自定义的类型处理器。这样我们就可以实现 DAO 到 Mapper 的映射。
然后,我们只需要定义各个 DAO 的接口即可。这里我们定义了一个顶层的接口,然后具体的 DAO 可以继承这个接口以添加自己的 DAO 方法:
public interface DAO<T extends AbstractPO> {
void insert(T entity) throws DAOException;
int update(T entity) throws DAOException;
int updatePOSelective(T entity) throws DAOException;
List<T> searchBySo(SearchObject so) throws DAOException;
<E> List<E> searchVosBySo(SearchObject so) throws DAOException;
long searchCountBySo(SearchObject so) throws DAOException;
void deleteByPrimaryKey(Long id) throws DAOException;
T selectByPrimaryKey(Long id) throws DAOException;
T selectVoByPrimaryKey(Long id) throws DAOException;
}
public interface TaskDAO extends DAO<Task> {
// 这个接口中需要增加的方法
}
然后,当然就是 DAO 的方法到 Mapper 文件中的 xml 元素的映射关系:
<mapper namespace="me.shouheng.service.dao.TaskDAO">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<insert id="insert" parameterType="Task">
insert into gt_task(
id,
created_time,
insert>
<update id="update" parameterType="Task">
update gt_task set
created_time=#{createdTime:BIGINT},
update>
mapper>
上面一个一个方法地去写未免过于繁琐,这种工作我们完全可以交给一些脚本程序来完成。后面我们会介绍我们的项目当中的脚本的实现和用途。
这种配置方式比前面的配置方式略微复杂一些。我们需要使用前面的 SqlSessionHolder 来获取当前线程对应的 SqlSession,然后使用它的方法进行数据库操作。这里我们定义了 BaseDAO,这是一个抽象类:
public abstract class BaseDAO<T extends AbstractPO> implements DAO<T> {
private static final String POSTFIX_SPLIT = ".";
private static final String POSTFIX_INSERT = "insert";
public BaseDAO() {
entityClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
protected SqlSession getSqlSession() {
return SqlSessionHolder.getSession();
}
protected String getStatementPrefix() {
return entityClass.getSimpleName() + POSTFIX_SPLIT;
}
@Override
public Long createPO(T entity) throws DAOException {
try {
Long start = System.currentTimeMillis();
getSqlSession().insert(getStatementPrefix() + POSTFIX_INSERT, entity);
logger.debug("insert cost is :" + (System.currentTimeMillis() - start));
return entity.getId();
} catch (Exception e) {
logger.error(getStatementPrefix() + " createPO", e);
throw new DAOException(e);
}
}
// ...
}
如上所示,这里我们需要先获取到当前 DAO 对应的 Entity 的类名称,然后使用它拼接成映射到 Mapper 的字符串。因此,这里对类名、Mapper 名有一些要求。当然,在这种开发方式中,我们也可以使用 Generator 来生成 Mapper 等各种文件来简化我们开发的复杂度。这种方式的好处就是我们可以在代码中对数据库操作进行包装。
RabbitMQ 可以用来进行服务器之间的解耦,本质上作用原理是生产者-消费者模式。比如服务器 A 和 B 以及 MQ 服务器,此时 A 发送一个消息到 MQ 服务器,然后 B 监听并获取到了消息之后进行处理。这样服务器 A 和 B 之间没有进行代码上面的耦合而只是通过 MQ 中维护的消息队列进行交互。这同时也意味着 A 和 B 服务器甚至不需要使用同一种语言进行开发。
使用 RabbitMQ 之前需要先安装 ErLang 环境,配置环境变量,然后就可以使用了。参考下面的链接来完成环境配置:
在使用 RabbitMQ 之前需要先添加 RabbitMQ 的依赖:
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
dependency>
然后,我们可以使用下面的代码来进行基本的测试:
private static final String QUEUE_NAME = "rabbitMQ.test";
@Test
public void testProducer() throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 相关信息
factory.setHost("localhost");
// factory.setUsername("lp");
// factory.setPassword("");
// factory.setPort(2088);
// 创建一个新的连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明一个队列
// channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello Rabbit MQ";
// 发送消息到队列中
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
log.debug("Producer Send +'{}'", message);
// 关闭通道和连接
channel.close();
connection.close();
}
@Test
public void testConsumer() throws IOException, TimeoutException, InterruptedException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置RabbitMQ地址
factory.setHost("localhost");
// 创建一个新的连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明要关注的队列
channel.queueDeclare(QUEUE_NAME, false, false, true, null);
log.debug("Customer Waiting Received messages");
// DefaultConsumer类实现了Consumer接口,通过传入一个频道,
// 告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
log.debug("Customer Received '{}'", message);
}
};
// 自动回复队列应答 -- RabbitMQ中的消息确认机制
channel.basicConsume(QUEUE_NAME, true, consumer);
Thread.sleep(10000);
}
以上我们定义了一个生产者和消费者测试方法。当我们在程序中集成 RabbitMQ 的时候实现的方式与之类似。
常用的服务端缓存主要有 Redis、Ehcache 和 Memcached。
Ehcache 与其他两个有明显的不同。Ehcache 与 java 程序是绑在一起的,直接在虚拟机中缓存,速度快,效率高,但是缓存共享麻烦,集群分布式应用不方便。
Redis 是一个独立的程序,我们需要使用 Jedis 客户端,通过 Socket 访问缓存服务,效率比 Ehcache 低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。
Memcached 与 Redis 类似,都是基于键值对的,但是 Redis 支持更多的数据结构。在 Memecached 与 Redis 之间进行选择的时候要基于具体的业务场景:
因此,如果是单个应用或者对缓存访问要求很高的应用,用 Ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,可以用 Redis 或者 Memecahce。
我们可以先在 Windows 上面安装 Redis 来进行学习。你可以参考下面这篇来了解如何在 Windows 上面安装 Redis:https://www.cnblogs.com/jaign/articles/7920588.html.
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
然后,为了使用 Redis 进行数据存储,我们需要初始化一个 RedisPool 对象,以用来获取 Jedis 客户端。因此,我们需要做如下的配置:
<bean class="redis.clients.jedis.JedisPool">
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
<constructor-arg name="host" value="${redis.host}"/>
<constructor-arg name="port" value="${redis.port}"/>
<constructor-arg name="timeout" value="${redis.timeout}"/>
<constructor-arg name="password" value="${redis.pass}"/>
bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}"/>
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
bean>
<context:property-placeholder location="classpath*:configs/redis.properties" ignore-unresolvable="true"/>
这里的配置也比较简单,就是初始化一个单例的 JedisPool。这个 JedisPool 对象的属性从 JedisPoolConfig 和 properties 文件两个部分得到。
然后,我们可以使用 JedisPool 来测试我们的 Redis 环境是否搭建成功:
@Autowired
private JedisPool jedisPool;
@Test
public void testRedisConnection() {
Jedis jedis = jedisPool.getResource();
jedis.set("the-key", "the-value");
String value = jedis.get("the-key");
Assert.assertEquals(value, "the-value");
}
上面的内容主要是用来集成 Redis 开发环境。经过上面的配置我们已经可以在程序中使用 Redis 来做缓存了。如果要对 SQL 进行缓存中需要通过自定义注解 + AOP 进行拦截即可。
这个原因是我们在项目当中引用了多个 properties 文件的原因。我们可以通过在
标签后面追加一个 ignore-unresolvable="true"
属性来避免这个问题。
这个标签的作用是:是否忽略解析不到的属性,如果不忽略,找不到将抛出异常。当 ignore-unresolvable
为 true 时,配置文件 ${}
找不到对应占位符的值不会报错,会直接赋值 ${}
;如果设为 false,会直接报错。
这里需要设置它为 true 的主要原因:同个模块中如果引用多个 properties,运行时出现 Could not resolve placeholder 'key'
的情况。原因是在加载第一个context:property-placeholder
时会扫描所有的 bean,而有的 bean 里面出现第二个 context:property-placeholder
引入的 properties 的占位符 ${key2}
,但此时还没有加载第二个 property-placeholder
,所以解析不了 ${key2}
。
除了追加上面的属性,也可以将多个 properties 文件合并来解决这个问题。
Ehcache 的快速集成可以参考官方的相关介绍:https://www.ehcache.org/.