spring-boot并不是全新的技术栈,而是整合了spring的很多组件,并且以约定优先的原则进行组合。使用boot我们不需要对冗杂的配置文件进行管理,主需要用它的注解便可启用大部分web开发中所需要的功能。本篇就是基于boot来配置jpa和静态文件访问,进行web应用的开发。
最原始的jsp页面在springboot中已经不在默认支持,spring-boot默认使用thymeleaf最为模板。当然我们也可以使用freemark或者velocity等其他后端模板。但是按照前后端分离的良好设计,我们最好采用静态页面作为前端模板,这样前后端完全分离,把数据处理逻辑写在程序并提供接口供前端调用。这样的设计更加灵活和清晰。
我们将讨论项目的结构、application配置文件、静态页面处理、自定义filter,listener,servlet以及拦截器的使用。最后集中讨论jpa的配置和操作以及如何进行单元测试和打包部署。
项目使用maven进行依赖管理和构建,整体结构如下图所示:
我们的HTML页面和资源文件都在resources/static下,打成jar包的时候static目录位于/BOOT-INF/classes/。
我们需要依赖下面这些包:
<?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>gxf.dev</groupId> <artifactId>topology</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <exclusions> <exclusion> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.10.RELEASE</version> <scope>test</scope> </dependency> </dependencies> <repositories> <repository> <id>nexus-aliyun</id> <name>Nexus aliyun</name> <layout>default</layout> <url>http://maven.aliyun.com/nexus/content/groups/public</url> <snapshots> <enabled>false</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>gxf.dev.topology.Application</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
spring-boot-starter-parent使我们项目的父pom。
spring-boot-starter-web提供嵌入式tomcat容器,从而使项目可以通过打成jar包的方式直接运行。
spring-boot-starter-data-jpa引入了jpa的支持。
spring-boot-test和junit配合做单元测试。
mysql-connector-java和HikariCP做数据库的连接池的操作。
spring-boot-maven-plugin插件能把项目和依赖的jar包以及资源文件和页面一起打成一个可运行的jar(运行在内嵌的tomcat)
package gxf.dev.topology; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; @SpringBootApplication @EnableAutoConfiguration @ServletComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
这里ServletComponentScan注解是启用servlet3的servler和filter以及listener的支持,下面会提到该用法。要注意的是: 不能引入@EnableWebMvc注解 ,否则需要重新配置视图和资源文件映射。这样就不符合我们的前后端分离的初衷了。
spring-boot默认会去classpath下面的/static/,/public/ ,/resources/目录去读取静态资源。因此按照约定优先的原则,我们直接把我们应用的页面和资源文件直接放在/static下面,如下图所示:
这样我们访问系统主页就会自动加载index.html,而且它所引用的资源文件也会在static/下开始加载。
我们在application配置文件中设置各种参数,它可以是传统的properties文件也可以使用yml来逐级配置。本文采用的第二种方式yml,如果不懂可以参考: baike.baidu.com/item/YAML/1… 。其内容如下:
server: port: 8080 context-path: /topology session: timeout: 30 tomcat: uri-encoding: utf-8 logging: level: root: info gxf.dev.topology: debug #当配置了loggin.path属性时,将在该路径下生成spring.log文件,即:此时使用默认的日志文件名spring.log #当配置了loggin.file属性时,将在指定路径下生成指定名称的日志文件。默认为项目相对路径,可以为logging.file指定绝对路径。 #path: /home/gongxufan/logs file: topology.log spring: jpa: show-sql: true open-in-view: false hibernate: naming: #配置ddl建表字段和实体字段一致 physical-strategy: gxf.dev.topology.config.RealNamingStrategyImpl ddl-auto: update properties: hibernate: format_sql: true show_sql: true dialect: org.hibernate.dialect.MySQL5Dialect datasource: url: jdbc:mysql://localhost:3306/topology driver-class-name: com.mysql.jdbc.Driver username: root password: qwe hikari: cachePrepStmts: true prepStmtCacheSize: 250 prepStmtCacheSqlLimit: 2048 useServerPrepStmts: true
使用idea开发工具在编辑器会有自动变量提示,这样非常方便进行参数的选择和查阅。
server节点可以配置容器的很多参数,比如:端口,访问路径、还有tomcat本身的一些参数。这里设置了session的超时以及编码等。
日志级别可以定义到具体的哪个包路径,日志文件的配置要注意:path和file配置一个就行,file默认会在程序工作目录下生成,也可以置顶绝对路径进行指定。
这里使用号称性能最牛逼的连接池hikaricp,具体配置可以参阅其官网: brettwooldridge.github.io/HikariCP/
这里主要注意下strategy的配置,关系到自动建表时的字段命名规则。默认会生成带_划线分割entity的字段名(骆驼峰式)。
package gxf.dev.topology.config; /** * ddl-auto选项开启的时候生成表的字段命名策略,默认会按照骆驼峰式风格用_隔开每个单词 * 这个类可以保证entity定义的字段名和数据表的字段一致 * @auth gongxufan * @Date 2016/8/3 **/ import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import java.io.Serializable; public class RealNamingStrategyImpl extends org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy implements Serializable { public static final PhysicalNamingStrategyStandardImpl INSTANCE = new PhysicalNamingStrategyStandardImpl(); @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { return new Identifier(name.getText(), name.isQuoted()); } @Override public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) { return new Identifier(name.getText(), name.isQuoted()); } }
1) 最新的spring-boot引入新的注解ServletComponentScan,使用它可以方便的配置Servlet3+的web组件。主要有下面这三个注解:
@WebServlet @WebFilter @WebListener
只要把这些注解标记组件即可完成注册。
package gxf.dev.topology.filter; import org.springframework.core.annotation.Order; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import java.io.IOException; /** * author:gongxufan * date:11/14/17 **/ @Order(1) @WebFilter(filterName = "loginFilter", urlPatterns = "/login") public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("login rquest"); chain.doFilter(request,response); } @Override public void destroy() { } }
package gxf.dev.topology.filter; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; /** * 自定义listener * Created by gongxufan on 2016/7/5. */ @WebListener public class SessionListener implements HttpSessionListener,HttpSessionAttributeListener { @Override public void sessionCreated(HttpSessionEvent httpSessionEvent) { System.out.println("init"); } @Override public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { System.out.println("destroy"); } @Override public void attributeAdded(HttpSessionBindingEvent se) { System.out.println(se.getName() + ":" + se.getValue()); } @Override public void attributeRemoved(HttpSessionBindingEvent se) { } @Override public void attributeReplaced(HttpSessionBindingEvent se) { } }
2) 拦截器的使用
拦截器不是Servlet规范的标准组件,它跟上面的三个组件不在一个处理链上。拦截器是spring使用AOP实现的,对controller执行前后可以进行干预,直接结束请求处理。而且拦截器只能对流经dispatcherServlet处理的请求才生效,静态资源就不会被拦截。
下面顶一个拦截器:
package gxf.dev.topology.filter; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * author:gongxufan * date:11/14/17 **/ public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println("LoginInterceptor.preHandle()在请求处理之前进行调用(Controller方法调用之前)"); // 只有返回true才会继续向下执行,返回false取消当前请求 return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println("LoginInterceptor.postHandle()请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)"); } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println("LoginInterceptor.afterCompletion()在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)"); } }
要想它生效则需要加入拦截器栈:
package gxf.dev.topology.config; import gxf.dev.topology.filter.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * author:gongxufan * date:11/14/17 **/ @Configuration public class WebMvcConfigurer extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { //在这可以配置controller的访问路径 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**"); super.addInterceptors(registry); } }
spring-boot已经集成了JPA的Repository封装,基于注解的事务处理等,我们只要按照常规的JPA使用方法即可。以Node表的操作为例:
package gxf.dev.topology.entity; import com.fasterxml.jackson.annotation.JsonInclude; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; /** * Created by gongxufan on 2014/11/20. */ @Entity @Table(name = "node") @JsonInclude(JsonInclude.Include.NON_NULL) public class Node implements Serializable { @Id private String id; private String elementType; private String x; private String y; private String width; private String height; private String alpha; private String rotate; private String scaleX; private String scaleY; private String strokeColor; private String fillColor; private String shadowColor; private String shadowOffsetX; private String shadowOffsetY; private String zIndex; private String text; private String font; private String fontColor; private String textPosition; private String textOffsetX; private String textOffsetY; private String borderRadius; private String deviceId; private String dataType; private String borderColor; private String offsetGap; private String childNodes; private String nodeImage; private String templateId; private String deviceA; private String deviceZ; private String lineType; private String direction; private String vmInstanceId; private String displayName; private String vmid; private String topoLevel; private String parentLevel; private Setring nextLevel; //getter&setter }
JsonInclude注解用于返回JOSN字符串是忽略为空的字段。
编写repository接口
package gxf.dev.topology.repository; import gxf.dev.topology.entity.Node; import org.springframework.data.repository.PagingAndSortingRepository; public interface NodeRepository extends PagingAndSortingRepository<Node, String> { }
编写Service
package gxf.dev.topology.service; import gxf.dev.topology.entity.Node; import gxf.dev.topology.repository.NodeRepository; import gxf.dev.topology.repository.SceneRepository; import gxf.dev.topology.repository.StageRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; /** * dao操作 * author:gongxufan * date:11/13/17 **/ @Component public class TopologyService { @Autowired private NodeRepository nodeRepository; @Autowired private SceneRepository sceneRepository; @Autowired private StageRepository stageRepository; @Transactional public Node saveNode(Node node) { return nodeRepository.save(node); } public Iterable<Node> getAll() { return nodeRepository.findAll(); } }
单元测试使用spring-boot-test和junit进行,需要用到下面的几个注解:
@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class)
测试代码如下:
import gxf.dev.topology.Application; import gxf.dev.topology.entity.Node; import gxf.dev.topology.repository.CustomSqlDao; import gxf.dev.topology.service.TopologyService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** * author:gongxufan * date:11/13/17 **/ @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class ServiceTest { @Autowired private TopologyService topologyService; @Autowired private CustomSqlDao customSqlDao; @Test public void testNode() { Node node = new Node(); node.setId("node:2"); node.setDisplayName("test1"); topologyService.saveNode(node); } @Test public void testNative(){ System.out.println(customSqlDao.querySqlObjects("select * from node")); System.out.println(customSqlDao.getMaxColumn("id","node")); } }
使用JPA进行单表操作确实很方便,但是对于多表连接的复杂查询可能不太方便。一般有两种方式弥补这个不足:
package gxf.dev.topology.repository; import com.mysql.jdbc.StringUtils; import org.hibernate.SQLQuery; import org.hibernate.criterion.CriteriaSpecification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Query; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 支持自定义SQL查询 * Created by gongxufan on 2016/3/17. */ @Component public class CustomSqlDao { @Autowired private EntityManagerFactory entityManagerFactory; public int getMaxColumn(final String filedName, final String tableName) { String sql = "select nvl(max(" + filedName + "), 0) as max_num from " + tableName; Map map = entityManagerFactory.getProperties(); String dialect = (String) map.get("hibernate.dialect"); //determine which database use if(!StringUtils.isNullOrEmpty(dialect)){ if(dialect.contains("MySQL")){ sql = "select ifnull(max(" + filedName + "), 0) as max_num from " + tableName; } if(dialect.contains("Oracle")){ sql = "select nvl(max(" + filedName + "), 0) as max_num from " + tableName; } } int maxID = 0; List<Map<String, Object>> list = this.querySqlObjects(sql); if (list.size() > 0) { Object maxNum = list.get(0).get("max_num"); if(maxNum instanceof Number) maxID = ((Number)maxNum).intValue(); if(maxNum instanceof String) maxID = Integer.valueOf((String)maxNum); } return maxID + 1; } public List<Map<String, Object>> querySqlObjects(String sql, Integer currentPage, Integer rowsInPage) { return this.querySqlObjects(sql, null, currentPage, rowsInPage); } public List<Map<String, Object>> querySqlObjects(String sql) { return this.querySqlObjects(sql, null, null, null); } public List<Map<String, Object>> querySqlObjects(String sql, Map params) { return this.querySqlObjects(sql, params, null, null); } @SuppressWarnings("unchecked") public List<Map<String, Object>> querySqlObjects(String sql, Object params, Integer currentPage, Integer rowsInPage) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query qry = entityManager.createNativeQuery(sql); SQLQuery s = qry.unwrap(SQLQuery.class); //设置参数 if (params != null) { if (params instanceof List) { List<Object> paramList = (List<Object>) params; for (int i = 0, size = paramList.size(); i < size; i++) { qry.setParameter(i + 1, paramList.get(i)); } } else if (params instanceof Map) { Map<String, Object> paramMap = (Map<String, Object>) params; Object o = null; for (String key : paramMap.keySet()) { o = paramMap.get(key); if (o != null) qry.setParameter(key, o); } } } if (currentPage != null && rowsInPage != null) {//判断是否有分页 // 起始对象位置 qry.setFirstResult(rowsInPage * (currentPage - 1)); // 查询对象个数 qry.setMaxResults(rowsInPage); } s.setResultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP); List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>(); try { List list = qry.getResultList(); resultList = s.list(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return resultList; } public int getCount(String sql) { String sqlCount = "select count(0) as count_num from " + sql; List<Map<String, Object>> list = this.querySqlObjects(sqlCount); if (list.size() > 0) { int countNum = ((BigDecimal) list.get(0).get("COUNT_NUM")).intValue(); return countNum; } else { return 0; } } /** * 处理sql语句 * * @param _strSql * @return */ public String toSql(String _strSql) { String strNewSql = _strSql; if (strNewSql != null) { strNewSql = regReplace("'", "''", strNewSql); } else { strNewSql = ""; } return strNewSql; } private String regReplace(String strFind, String strReplacement, String strOld) { String strNew = strOld; Pattern p = null; Matcher m = null; try { p = Pattern.compile(strFind); m = p.matcher(strOld); strNew = m.replaceAll(strReplacement); } catch (Exception e) { } return strNew; } /** * 根据hql语句查询数据 * * @param hql * @return */ @SuppressWarnings("rawtypes") public List queryForList(String hql, List<Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); List list = null; try { if (params != null && !params.isEmpty()) { for (int i = 0, size = params.size(); i < size; i++) { query.setParameter(i + 1, params.get(i)); } } list = query.getResultList(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return list; } @SuppressWarnings("rawtypes") public List queryByMapParams(String hql, Map<String, Object> params, Integer currentPage, Integer pageSize) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); List list = null; try { if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } if (currentPage != null && pageSize != null) { query.setFirstResult((currentPage - 1) * pageSize); query.setMaxResults(pageSize); } list = query.getResultList(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return list; } @SuppressWarnings("rawtypes") public List queryByMapParams(String hql, Map<String, Object> params) { return queryByMapParams(hql, params, null, null); } @SuppressWarnings("rawtypes") public List queryForList(String hql) { return queryForList(hql, null); } /** * 查询总数 * * @param hql * @param params * @return */ public Long queryCount(String hql, Map<String, Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Query query = entityManager.createQuery(hql); Long count = null; try { if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } count = (Long) query.getSingleResult(); } catch (Exception e) { e.printStackTrace(); } finally { entityManager.close(); } return count; } /** * 查询总数 * * @param sql * @param params * @return */ public Integer queryCountBySql(String sql, Map<String, Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); Integer count = null; try { Query query = entityManager.createNativeQuery(sql); if (params != null && !params.isEmpty()) { for (Map.Entry<String, Object> entry : params.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } Object obj = query.getSingleResult(); if (obj instanceof BigDecimal) { count = ((BigDecimal) obj).intValue(); } else { count = (Integer) obj; } } finally { if (entityManager != null) { entityManager.close(); } } return count; } /** * select count(*) from table * * @param sql * @param params * @return */ public int executeSql(String sql, List<Object> params) { EntityManager entityManager = entityManagerFactory.createEntityManager(); try { Query query = entityManager.createNativeQuery(sql); if (params != null && !params.isEmpty()) { for (int i = 0, size = params.size(); i < size; i++) { query.setParameter(i + 1, params.get(i)); } } return query.executeUpdate(); } finally { if (entityManager != null) { entityManager.close(); } } } }
我们在service层注入,然后就可以根据输入条件拼接好sql或者hql来进行各种操作。这种方式灵活而且也不需要手动写分页代码,使用hibernate封装好的机制即可。
使用boot可以快速搭建一个前后端开发的骨架,里面有很多自动的配置和约定。虽然boot不是新的一个技术栈,但是它要求我们对各个组件都要比较熟悉,不然对它的运行机制和约定配置会感到很困惑。而使用JPA进行数据库操作也是利弊参半,需要自己权衡。
项目代码: github.com/gongxufan/t…