Advertisement
Guest User

深入实践Spring Boot

a guest
Mar 19th, 2019
128
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.10 KB | None | 0 0
  1.  
  2.  
  3. ```java
  4. // 代码清单4-4 使用@Query自定义查询
  5.  
  6. @Repository
  7. public interface UserRepository extends JpaRepository {
  8. @Query("select t from User t where t.name =?1 and t.email =?2")
  9. User findByNameAndEmail(String name, String email);
  10. @Query("select t from User t where t.name like :name")
  11. Page findByName(@Param("name") String name, Pageable pageRequest);
  12. }
  13. 4.2.1 扩展JPA接口
  14. 首先创建一个接口,继承于JpaRepository,并将接口标记为@NoRepositoryBean,以防被当作一般的Repository调用,如代码清单4-5所示。接口ExpandJpaRepository不仅扩展了JPA原来的findOne、findAll、count等方法,而且增加了deleteByIds、get-EntityClass、nativeQuery4Map等方法,其中nativeQuery4Map用来执行原生的复杂的SQL查询语句。
  15. 代码清单4-5 扩展JPA接口定义
  16. @NoRepositoryBean
  17. public interface ExpandJpaRepository extends JpaRepository {
  18. T findOne(String condition, Object... objects);
  19. List findAll(String condition, Object... objects);
  20. List findAll(Iterable predicates, Operator operator);
  21. List findAll(Iterable predicates, Operator operator, Sort sort);
  22. Page findAll(Iterable predicates, Operator operator, Pageable
  23. pageable);
  24. long count(Iterable predicates, Operator operator);
  25. List findAll(String condition, Sort sort, Object... objects);
  26. Page findAll(String condition, Pageable pageable, Object... objects);
  27. long count(String condition, Object... objects);
  28. void deleteByIds(Iterable ids);
  29. Class getEntityClass();
  30. List> nativeQuery4Map(String sql);
  31. Page nativeQuery4Map(String sql, Pageable pageable);
  32. Object nativeQuery4Object(String sql);
  33. }
  34. 这一接口的所有声明方法,必须由我们来实现。为了节省篇幅,只列出实现的部分代码,如代码清单4-6所示。完整的代码可以通过检出实例工程查看。实现代码中使用了JPQL查询语言(Java Persistence Query Language),它是JPA的查询语句规范。
  35. 代码清单4-6 扩展JPA接口实现
  36. public class ExpandJpaRepositoryImpl extends SimpleJpaRepository implements ExpandJpaRepository {
  37. private final EntityManager entityManager;
  38. private final JpaEntityInformation entityInformation;
  39. public ExpandJpaRepositoryImpl(JpaEntityInformation entityInformation,
  40. EntityManager entityManager) {
  41. super(entityInformation, entityManager);
  42. this.entityManager = entityManager;
  43. this.entityInformation = entityInformation;
  44. }
  45. @Override
  46. public T findOne(String condition, Object... values) {
  47. if(isEmpty(condition)){
  48. throw new NullPointerException("
  49. 条件不能为空
  50. !");
  51. }
  52. T result = null;
  53. try {
  54. result = (T) createQuery(condition, values).getSingleResult();
  55. } catch (NoResultException e) {
  56. e.printStackTrace();
  57. }
  58. return result;
  59. }
  60. @Override
  61. public List findAll(Iterable predicates, Operator operator) {
  62. return new JpqlQueryHolder(predicates,operator).createQuery().getResult
  63. List();
  64. }
  65. @Override
  66. public List findAll(Iterable predicates, Operator operator, Sort sort) {
  67. return new JpqlQueryHolder(predicates,operator,sort).createQuery().getResult
  68. List();
  69. }
  70. @Override
  71. public Page findAll(Iterable predicates, Operator operator, Pageable
  72. pageable) {
  73. if(pageable==null){
  74. return new PageImpl((List) findAll(predicates,operator));
  75. }
  76. Long total = count(predicates,operator);
  77. Query query = new JpqlQueryHolder(predicates,operator,pageable.getSort()).
  78. createQuery();
  79. query.setFirstResult(pageable.getOffset());
  80. query.setMaxResults(pageable.getPageSize());
  81. List content = total > pageable.getOffset() ? query.getResultList() :
  82. Collections. emptyList();
  83. return new PageImpl(content, pageable, total);
  84. }
  85. ......
  86. }
  87. 因为自定义的接口继承于JpaRepository,所以不但具有自定义的一些功能,而且拥有JPA原来的所有功能,它的继承关系如图4-3所示。
  88. 图4-3 扩展JPA接口的继承关系
  89.  
  90. 图4-3 扩展JPA接口的继承关系
  91. 4.2.2 装配自定义的扩展接口
  92. 自定义的接口必须在程序启动时装配,才能正常使用。首先,创建一个装配类ExpandJpaRepository-FactoryBean,继承于JpaRepositoryFactory-Bean,用来加载自定义的扩展接口,如代码清单4-7所示。其中getTargetRepository返回自定义的接口实现:ExpandJpaRepositoryImpl。
  93. 代码清单4-7 JPA扩展接口装配类
  94. public class ExpandJpaRepositoryFactoryBean, T, ID extends Serializable>
  95. extends JpaRepositoryFactoryBean {
  96. protected RepositoryFactorySupport createRepositoryFactory(
  97. EntityManager entityManager) {
  98. return new ExpandJpaRepositoryFactory(entityManager);
  99. }
  100. private static class ExpandJpaRepositoryFactory
  101.  
  102. extends JpaRepositoryFactory {
  103. private final EntityManager entityManager;
  104. public ExpandJpaRepositoryFactory(EntityManager entityManager) {
  105. super(entityManager);
  106. this.entityManager = entityManager;
  107. }
  108. protected Object getTargetRepository(RepositoryMetadata metadata) {
  109. JpaEntityInformation entityInformation = (JpaEntity
  110. Information) getEntityInformation(metadata.getDomainType());
  111. return new ExpandJpaRepositoryImpl(entityInformation, entity
  112. Manager);
  113. }
  114. protected Class getRepositoryBaseClass(RepositoryMetadata metadata) {
  115. return ExpandJpaRepositoryImpl.class;
  116. }
  117. }
  118. }
  119. 然后,在JPA配置类中,通过@EnableJpaRepositories加载定义的装配类ExpandJpa-RepositoryFactoryBean,如代码清单4-8所示。其中,“com.**.repository”为定义接口的资源库路径,“com.**.entity”为实体模型的路径。
  120. 代码清单4-8 JPA配置类
  121. @Order(Ordered.HIGHEST_PRECEDENCE)
  122. @Configuration
  123. @EnableTransactionManagement(proxyTargetClass = true)
  124. @EnableJpaRepositories(basePackages = "com.**.repository",repositoryFactoryBeanClass = ExpandJpaRepositoryFactoryBean.class)
  125. @EntityScan(basePackages = "com.**.entity")
  126. public class JpaConfiguration {
  127. @Bean
  128. PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor(){
  129. return new PersistenceExceptionTranslationPostProcessor();
  130. }
  131. }
  132. 4.2.3 使用扩展接口
  133. 现在来做实体的持久化,这样就可以直接使用自定义的扩展接口了。如代码清单4-9所示,资源库接口UserRepository继承的就是前面定义的接口ExpandJpaRepository。
  134. 代码清单4-9 使用扩展接口做持久化
  135. @Repository
  136. public interface UserRepository extends ExpandJpaRepository {
  137. @Query("select t from User t where t.name =?1 and t.email =?2")
  138. User findByNameAndEmail(String name, String email);
  139. @Query("select t from User t where t.name like :name")
  140. Page findByName(@Param("name") String name, Pageable pageRequest);
  141. }
  142. 使用JPA扩展接口与使用原来的JPA接口一样,调用方法基本相同,只不过有些方法被赋予更为丰富的功能,可以更加灵活地使用。代码清单4-10是一个使用扩展接口的分页查询,使用PredicateBuilder来构造一个查询参数的对象,它可以包含更多的查询参数。
  143. 代码清单4-10 使用扩展JPA接口的分页查询
  144. @Service
  145. public class UserService {
  146. @Autowired
  147. private UserRepository userRepository;
  148. public Page findPage(UserQo userQo){
  149. Pageable pageable = new PageRequest(userQo.getPage(), userQo.getSize(),
  150. new Sort(Sort.Direction.ASC, "id"));
  151. PredicateBuilder pb = new PredicateBuilder();
  152. if (!StringUtils.isEmpty(userQo.getName())) {
  153. pb.add("name","%" + userQo.getName() + "%", LinkEnum.LIKE);
  154. }
  155. if (!StringUtils.isEmpty(userQo.getCreatedateStart())) {
  156. pb.add("createdate",userQo.getCreatedateStart(), LinkEnum.GE);
  157. }
  158. if (!StringUtils.isEmpty(userQo.getCreatedateEnd())) {
  159. pb.add("createdate",userQo.getCreatedateEnd(), LinkEnum.LE);
  160. }
  161. return userRepository.findAll(pb.build(), Operator.AND, pageable);
  162. }
  163. }
  164. 4.3 使用Redis做缓存
  165. 在数据库使用中,数据查询是最大的性能开销。如果能借助Redis作为辅助缓存,将可以极大地提高数据库的访问性能。使用Redis做缓存,一方面可以像第2章介绍的使用Redis那样调用,另一方面,可以使用注解的方式来调用,这种方式更加简单,代码也更加简洁。
  166. 需要注意的是,Redis是一个具有持久化功能的数据库系统,若使用默认配置,存取的数据就会永久地保存在磁盘中。如果只是使用Redis来做缓存,并不需要Redis永久保存数据,可以设定在Redis保存数据的期限来实现,这样,过期的数据将自动从Redis数据库中清除。这不但能很好地利用Redis的快速存取功能,而且能彻底减轻Redis的负担。作为缓存使用的数据,最初就是从数据库中查询出来的,所以完全没有必要再做一次永久保存。始终让Redis保持轻装上阵,才能最好地发挥它的性能优势。
  167. 4.3.1 使用Spring Cache注解
  168. 结构简单的对象,即没有包含其他对象的实体,可以使用Spring Cache注解的方式来使用Redis缓存。要使用注解的方式调用缓存,必须在配置类中启用Spring Cache,如代码清单4-11所示。其中setDefaultExpiration指定了数据在Redis数据库中的有效期限。
  169. 代码清单4-11 Spring Cache配置
  170. @Configuration
  171. @EnableCaching
  172. public class RedisConfig extends CachingConfigurerSupport {
  173. @Bean
  174. public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate
  175. redisTemplate) {
  176. RedisCacheManager manager = new RedisCacheManager(redisTemplate);
  177. manager.setDefaultExpiration(43200);// 12
  178. 小时
  179. return manager;
  180. }
  181. }
  182. 这样,就可以在对数据接口的调用中,对增删查改加入如代码清单4-12所示的注解,自动增加缓存的创建、修改和删除等功能。其中注解@Cacheable为存取缓存,注解@CachePut为修改缓存,如果不存在则创建,注解@CacheEvict为删除缓存,当删除数据时,如果缓存还存在,就必须删除,各个注解中的value参数是一个key的前缀,并由keyGenerator按一定的规则生成一个唯一标识。
  183. 代码清单4-12 用注解方式使用Redis做缓存
  184. @Service
  185. public class RoleService {
  186. @Autowired
  187. private RoleRepository roleRepository;
  188. @Autowired
  189. private RoleRedis roleRedis;
  190. @Cacheable(value = "mysql:findById:role", keyGenerator = "simpleKey")
  191. public Role findById(Long id) {
  192. return roleRepository.findOne(id);
  193. }
  194. @CachePut(value = "mysql:findById:role", keyGenerator = "objectId")
  195. public Role create(Role role) {
  196. return roleRepository.save(role);
  197. }
  198. @CachePut(value = "mysql:findById:role", keyGenerator = "objectId")
  199. public Role update(Role role) {
  200. return roleRepository.save(role);
  201. }
  202. @CacheEvict(value = "mysql:findById:role", keyGenerator = "simpleKey")
  203. public void delete(Long id) {
  204. roleRepository.delete(id);
  205. }
  206. ......
  207. }
  208. 对于key的生成规则,使用如代码清单4-13所示的方法来实现,这里主要使用了调用者本身对象的ID属性来保证它的唯一性,其中simpleKey和objectId都是提取调用者本身的类名字和参数id作为唯一标识。
  209. 代码清单4-13 生成cache的key
  210. @Bean
  211. public KeyGenerator simpleKey(){
  212. return new KeyGenerator() {
  213. @Override
  214. public Object generate(Object target, Method method, Object... params) {
  215. StringBuilder sb = new StringBuilder();
  216. sb.append(target.getClass().getName()+":");
  217. for (Object obj : params) {
  218. sb.append(obj.toString());
  219. }
  220. return sb.toString();
  221. }
  222. };
  223. }
  224. @Bean
  225. public KeyGenerator objectId(){
  226. return new KeyGenerator() {
  227. @Override
  228. public Object generate(Object target, Method method, Object... params){
  229. StringBuilder sb = new StringBuilder();
  230. sb.append(target.getClass().getName()+":");
  231. try {
  232. sb.append(params[0].getClass().getMethod("getId", null).
  233. invoke(params[0], null).toString());
  234. }catch (NoSuchMethodException no){
  235. no.printStackTrace();
  236. }catch(IllegalAccessException il){
  237. il.printStackTrace();
  238. }catch(InvocationTargetException iv){
  239. iv.printStackTrace();
  240. }
  241. return sb.toString();
  242. }
  243. };
  244. }
  245. 4.3.2 使用RedisTemplate
  246. 由于使用Spring Cache注解的方法使用Redis缓存,只能对简单对象进行系列化操作,所以对于像实体User这样的包含了一定关系的复杂对象,或其他集合、列表对象等,就不能使用简单注解的方法来实现了,还要像第2章中介绍的方法那样使用RedisTemplate来调用Redis,其使用的效果和上面使用Cache注解的效果相同,只不过实现方法完全不同。
  247. 代码清单4-14使用RedisTemplate实现了对Redis的调用。这种方式可以很方便地对列表对象进行系列化,在数据存取时使用Json进行格式转换。这里使用分钟作为时间单位来设定数据在Redis中保存的有效期限。
  248. 代码清单4-14 使用RedisTemplate
  249. @Repository
  250. public class UserRedis {
  251. @Autowired
  252. private RedisTemplate redisTemplate;
  253. public void add(String key, Long time, User user) {
  254. Gson gson = new Gson();
  255. redisTemplate.opsForValue().set(key, gson.toJson(user), time, TimeUnit.
  256. MINUTES);
  257. }
  258. public void add(String key, Long time, List users) {
  259. Gson gson = new Gson();
  260. redisTemplate.opsForValue().set(key, gson.toJson(users), time, TimeUnit.
  261. MINUTES);
  262. }
  263. public User get(String key) {
  264. Gson gson = new Gson();
  265. User user = null;
  266. String json = redisTemplate.opsForValue().get(key);
  267. if(!StringUtils.isEmpty(json))
  268. user = gson.fromJson(json, User.class);
  269. return user;
  270. }
  271. public List getList(String key) {
  272. Gson gson = new Gson();
  273. List ts = null;
  274. String listJson = redisTemplate.opsForValue().get(key);
  275. if(!StringUtils.isEmpty(listJson))
  276. ts = gson.fromJson(listJson, new TypeToken>(){}.getType());
  277. return ts;
  278. }
  279. public void delete(String key){
  280. redisTemplate.opsForValue().getOperations().delete(key);
  281. }
  282. }
  283. 然后编写如代码清单4-15所示的代码来使用Redis缓存。即在使用原来数据库的增删查改过程中,同时使用Redis进行辅助存取,以达到提升访问速度的目的,从而缓解对原来数据库的访问压力。这样,访问一条数据时,首先从Redis读取,如果存在则不再到MySQL中读取,如果不存在再到MySQL读取,并将读取的结果暂时保存在Redis中。
  284. 代码清单4-15 在数据服务中使用Redis作为辅助缓存
  285. @Service
  286. public class UserService {
  287. @Autowired
  288. private UserRepository userRepository;
  289. @Autowired
  290. private UserRedis userRedis;
  291. private static final String keyHead = "mysql:get:user:";
  292. public User findById(Long id) {
  293. User user = userRedis.get(keyHead + id);
  294. if(user == null){
  295. user = userRepository.findOne(id);
  296. if(user != null)
  297. userRedis.add(keyHead + id, 30L, user);
  298. }
  299. return user;
  300. }
  301. public User create(User user) {
  302. User newUser = userRepository.save(user);
  303. if(newUser != null)
  304. userRedis.add(keyHead + newUser.getId(), 30L, newUser);
  305. return newUser;
  306. }
  307. public User update(User user) {
  308. if(user != null) {
  309. userRedis.delete(keyHead + user.getId());
  310. userRedis.add(keyHead + user.getId(), 30L, user);
  311. }
  312. return userRepository.save(user);
  313. }
  314. public void delete(Long id) {
  315. userRedis.delete(keyHead + id);
  316. userRepository.delete(id);
  317. }
  318. 上面使用Redis缓存的两种方法,可以在一个应用中混合使用。但不管怎么使用,对于控制器来说都是完全透明的,控制器对数据接口的调用还是像以前一样,它并不清楚数据接口后端是否启用了缓存,如代码清单4-16所示。
  319. 代码清单4-16 控制器使用数据接口
  320. @Autowired
  321. private UserService userService;
  322. @RequestMapping(value="/{id}")
  323. public String show(ModelMap model,@PathVariable Long id) {
  324. User user = userService.findById(id);
  325. model.addAttribute("user",user);
  326. return "user/show";
  327. }
  328. 使用缓存之后,大量的查询语句就从原来的数据库服务器中,转移到了高效的Redis服务器中执行,这将在很大程度上减轻原来数据库服务器的压力,并且提升查询的反应速度和效率。所以在很大的程度上,系统性能就得到了很好的改善。
  329. 4.4 Web应用模块
  330. 对于上面一些功能的实现,最后使用一个Web应用来调用,以验证使用Druid连接池和使用Redis缓存的效果,同时可以体验使用JPA扩展接口更为丰富的功能。
  331. 4.4.1 引用数据管理模块
  332. 实例工程中的Web应用模块将引用数据管理模块,而数据管理模块使用了第2章实例工程中MySQL模块的实体-关系模型设计,即使用部门、用户和角色三个实体,如图4-4所示。实体的建模还与第2章中使用的方法一样,没有做任何修改。至于实体的持久化,如前所述,只要在原来的持久化中改变资源库接口定义中继承于自定义的扩展接口即可。
  333. 图4-4 实体-关系模型设计
  334.  
  335. 图4-4 实体-关系模型设计
  336. 4.4.2 Web应用配置
  337. Web应用的界面设计使用第3章的设计来实现。这里,主要实现对部门、用户和角色三个实体的数据进行增删查改的管理。
  338. 在Web应用模块的配置文件application.yml中,配置连接MySQL和Redis服务器的一些参数,如代码清单4-17所示。
  339. 代码清单4-17 Web应用配置
  340. server:
  341. port: 80
  342. tomcat:
  343. uri-encoding: UTF-8
  344. spring:
  345. datasource:
  346. type: com.alibaba.druid.pool.DruidDataSource
  347. driver-class-name: com.mysql.jdbc.Driver
  348. url: jdbc:mysql:// localhost:3306/test?characterEncoding=utf8
  349. username: root
  350. password: 12345678
  351. #
  352. 初始化大小,最小,最大
  353. initialSize: 5
  354. minIdle: 5
  355. maxActive: 20
  356. #
  357. 配置获取连接等待超时的时间
  358. maxWait: 60000
  359. #
  360. 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  361. timeBetweenEvictionRunsMillis: 60000
  362. #
  363. 配置一个连接在池中的最小生存时间,单位是毫秒
  364. minEvictableIdleTimeMillis: 300000
  365. validationQuery: SELECT 1 FROM DUAL
  366. testWhileIdle: true
  367. testOnBorrow: false
  368. testOnReturn: false
  369. #
  370. 打开
  371. PSCache
  372. ,并且指定每个连接上
  373. PSCache
  374. 的大小
  375. poolPreparedStatements: true
  376. maxPoolPreparedStatementPerConnectionSize: 20
  377. #
  378. 配置监控统计拦截的
  379. filters
  380. ,去掉后监控界面
  381. SQl
  382. 将无法统计,
  383. 'wall'
  384. 用于防火墙
  385. filters: stat,wall,log4j
  386. #
  387. 通过
  388. connectProperties
  389. 属性来打开
  390. mergeSql
  391. 功能;慢
  392. SQL
  393. 记录
  394. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  395. #
  396. 合并多个
  397. DruidDataSource
  398. 的监控数据
  399. #useGlobalDataSourceStat=true
  400. jpa:
  401. database: MYSQL
  402. show-sql: true
  403. ## Hibernate ddl auto (validate|create|create-drop|update)
  404. hibernate:
  405. ddl-auto: update
  406. naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
  407. properties:
  408. hibernate:
  409. dialect: org.hibernate.dialect.MySQL5Dialect
  410. ## redis
  411. redis:
  412. host: 192.168.1.214
  413. port: 6379
  414. pool:
  415. max-idle: 8
  416. min-idle: 0
  417. max-active: 8
  418. max-wait: -1
  419. 启动应用后,运行效果如图4-5所示。除了分页数据没有做缓存之外,其他查询都做了缓存处理。在控制台上可以看到执行的SQL查询语句,一个查询,比如查看用户,如果在控制台上没有看到输出查询语句,就可以说明是调用了Redis缓存。
  420. 关于使用缓存的情况,也可以登录安装Redis的服务器,使用下列指令,查看当前所有的key。
  421. #redis-cli
  422. >keys *
  423. 下载一个Redis客户端,可以更加直观地查看Redis服务器的情况,如图4-6所示。
  424. 图4-5 Web应用运行效果
  425.  
  426. 图4-5 Web应用运行效果
  427. 图4-6 Redis客户端
  428.  
  429. 图4-6 Redis客户端
  430. 4.5 运行与发布
  431. 本章实例的完整代码可以直接在IDEA中通过GitHub检出:https://github.com/chen-fromsz/spring-boot-dbup.git。
  432. 检出工程后,可以运行Web应用模块website进行测试。在IDEA的Run/Configura-tion中增加一个Spring Boot配置,模块选择website,工作目录选择website模块所在的路径,主程序选择com.test.website.WebApplication,并将配置保存为webapp。
  433. 在MySQL服务器中创建一个数据库:test,配置Web应用模块website的配置文件application.yml中连接MySQL服务器的url、username、password,以及Redis的host和port。然后运行配置项目webapp,启动完成后,在浏览器中打开网址:http://127.0.0.1。
  434. 注意 因为localhost不能加入Druid的监控服务器的白名单中,所以使用localhost可能不能正常访问。而使用域名的方式是可以的,只要把域名所指向的IP加入Druid的白名单中即可。
  435. 如果要打包发布,可以在IDEA的Run/Configuration中增加一个Maven配置项目,工作目录选择工程根目录spring-boot-dbup所在的路径,在命令行中输入指令clean package,然后将配置项目保存为mvn。或者直接打开一个命令行窗口,切换到工程根目录所在路径,执行下列Maven指令:
  436. mvn clean package
  437. 打包完成后,在命令行窗口中切换到模块website的target目录中,输入下列指令可运行应用:
  438. java
  439. jar website-1.0-SNAPSHOT.jar
  440. 4.6 小结
  441. 本章使用Druid连接池和Redis数据库作为缓存,提升了关系型数据库的访问性能,并且通过扩展全局的JPA接口,丰富了资源库的调用功能。
  442. 对于大数据时代的互联网应用来说,要从根本上提升数据库的性能,主要还在于数据库本身的设计和配置上,例如使用分布式设计的集群系统,通过合理的配置和组装,可以达到横向扩展的目的,以后通过增加设备的方式,可以随时扩充数据库的容量和提高其访问性能。
  443. 有了完备的数据库访问功能和漂亮的操作界面之后,一个应用中更重要的设计就是安全设计了。下一章将介绍使用Spring Security来为一个应用实现安全设计,从而实现用户认证和权限管理方面的功能。
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement