跳至主要內容

SSJ开发框架整合

wangdx大约 23 分钟

SSJ 案例实现说明

数据 CRUD 处理

  • 在企业级应用开发中,会将所有的数据保存在关系型数据库之中,这样即满足了持久化存储要求,同时又满足了结构化的查询需求,而作为企业级应用的 Java 语言,也提供了专属的 JDBC 开发规范,只需要配置上所使用数据库的驱动程序,就可以方便的实现数据库更新与查询操作

基于 JPA 项目开发

  • 在 WEB 开发中如果要想实现合理的 CRUD 处理机制,往往需要进行合理的分层设计,利用数据层实现 SQL 命令的包装,而后相关的数据处理逻辑通过业务层整合,而最终的数据展示则会交由控制层分发到视图层完成,但是传统的 JDBC 开发方式毕竟过于琐碎同时考虑到代码的维护问题,此时就可以基于开发框架进行项目的实现

案例功能分析

  • 使用 JPA 进行数据库的开发,可以简化原生 JDBC 的繁琐操作细节,同时利用 SpringData 技术也便于数据层操作方法的定义与实现。考虑到数据库查询性能的影响,可以在一些常用的数据上通过 SpringCache 技术实现数据缓存配置,以减少数据查询操作所带来的性能损耗。为了进步便于项目内存的管理,本次将使用 Memcached 缓存数据库实现分布式缓存,而本次的程序开发也将基于此技术架构实现

搭建 SSJ 开发环境

创建 ssj-case 子模块

  • 本次的项目开发将基于 IDEA 工具实现,同时采用主流 Bean 配置的方式定义 SpringMVC、SpringCache、Memcached、HikariCP、SpringDataJPA 等项目组件
1、
package com.yootk.ssj.context.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScans({
        @ComponentScan("com.yootk.common.web.config"),
        @ComponentScan("com.yootk.ssj.config"),
        @ComponentScan("com.yootk.ssj.service"),
        @ComponentScan("com.yootk.ssj.dao")
}) // common模块中的类
public class SpringApplicationContextConfig {}


2、
# 【JDBC】配置JDBC驱动程序
yootk.database.driverClassName=com.mysql.cj.jdbc.Driver
# 【JDBC】配置JDBC连接地址
yootk.database.jdbcUrl=jdbc:mysql://localhost:3306/yootk_ssj
# 【JDBC】数据库用户名
yootk.database.username=root
# 【JDBC】配置数据库密码
yootk.database.password=mysqladmin
# 【HikariCP】定义连接超时时间
yootk.database.connectionTimeOut=3000
# 【HikariCP】是否为只读操作
yootk.database.readOnly=false
# 【HikariCP】定义一个连接最小维持的时长
yootk.database.pool.idleTimeOut=3000
# 【HikariCP】定义一个连接最大的保存时间
yootk.database.pool.maxLifetime=6000
# 【HikariCP】定义数据库连接池最大长度
yootk.database.pool.maximumPoolSize=60
# 【HikariCP】定义数据库连接池最小的维持数量
yootk.database.pool.minimumIdle=20

3、
# 【JPA使用环境】开启DDL自动更换操作机制,如果发现数据表的结构有问题时,自动进行更新
hibernate.hbm2ddl.auto=update
# 【JPA使用环境】是否要显示出每次执行的SQL命令
hibernate.show_sql=true
# 【JPA使用环境】是否采用格式化的方式进行SQL显示
hibernate.format_sql=false
# 【JPA二级缓存】启用二级缓存配置
hibernate.cache.use_second_level_cache=true
# 【JPA二级缓存】二级缓存的工厂类,本次为jcache
hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
# 【JPA二级缓存】配置JSR-107缓存标准实现子类
hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider


4、
package com.yootk.ssj.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

@Configuration // 配置类
@PropertySource("classpath:config/database.properties") // 读取资源文件
public class DataSourceConfig {
    @Value("${yootk.database.driverClassName}") // 绑定资源文件中的配置数据项
    private String driverClassName; // JDBC驱动程序
    @Value("${yootk.database.jdbcUrl}") // 绑定资源文件中的配置数据项
    private String jdbcUrl; // JDBC连接地址
    @Value("${yootk.database.username}") // 绑定资源文件中的配置数据项
    private String username; // JDBC连接用户名
    @Value("${yootk.database.password}") // 绑定资源文件中的配置数据项
    private String password; // JDBC连接密码
    @Value("${yootk.database.connectionTimeOut}") // 绑定资源文件中的配置数据项
    private long connectionTimeout; // JDBC连接超时时间
    @Value("${yootk.database.readOnly}") // 绑定资源文件中的配置数据项
    private boolean readOnly; // 是否为只读数据库
    @Value("${yootk.database.pool.idleTimeOut}") // 绑定资源文件中的配置数据项
    private long idleTimeOut; // 连接池之中的,一个连接的存活最小时间
    @Value("${yootk.database.pool.maxLifetime}") // 绑定资源文件中的配置数据项
    private long maxLifetime; // 连接池之中的,一个连接的存活最长时间
    @Value("${yootk.database.pool.maximumPoolSize}") // 绑定资源文件中的配置数据项
    private int maximumPoolSize; // 连接池最大维持的长度
    @Value("${yootk.database.pool.minimumIdle}") // 绑定资源文件中的配置数据项
    private int minimumIdle; // 连接池最小维持的长度
    @Bean("dataSource") // 配置Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(this.driverClassName);
        dataSource.setJdbcUrl(this.jdbcUrl);
        dataSource.setUsername(this.username);
        dataSource.setPassword(this.password);
        dataSource.setConnectionTimeout(this.connectionTimeout);
        dataSource.setReadOnly(this.readOnly);
        dataSource.setIdleTimeout(this.idleTimeOut);
        dataSource.setMaxLifetime(this.maxLifetime);
        dataSource.setMaximumPoolSize(this.maximumPoolSize);
        dataSource.setMinimumIdle(this.minimumIdle);
        return dataSource;
    }
}


5、
package com.yootk.test;

import com.yootk.ssj.context.config.SpringApplicationContextConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.sql.DataSource;

@ContextConfiguration(classes = {SpringApplicationContextConfig.class})
@ExtendWith(SpringExtension.class)
public class TestDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestDataSource.class);
    @Autowired
    private DataSource dataSource;
    @Test
    public void testConnection() throws Exception {
        LOGGER.info("数据库连接对象:{}", this.dataSource.getConnection());
    }
}


6、
package com.yootk.ssj.config;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@Aspect
public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // 如果此时采用的是其他的事务管理器,会出现无法正常实现数据更新操作的问题
        JpaTransactionManager transactionManager =
                new JpaTransactionManager();
        transactionManager.setDataSource(dataSource); // 设置管理的数据源
        return transactionManager;
    }
    @Bean("txAdvice")
    public TransactionInterceptor transactionInterceptor(
            TransactionManager transactionManager) {
        RuleBasedTransactionAttribute readAttribute = new RuleBasedTransactionAttribute();
        readAttribute.setReadOnly(true); // 只读事务
        readAttribute.setPropagationBehavior(
                TransactionDefinition.PROPAGATION_NOT_SUPPORTED); // 非事务运行
        RuleBasedTransactionAttribute requiredAttribute = new RuleBasedTransactionAttribute();
        requiredAttribute.setPropagationBehavior(
                TransactionDefinition.PROPAGATION_REQUIRED); // 开启事务
        Map<String, TransactionAttribute> attributeMap = new HashMap<>(); // 保存方法映射
        attributeMap.put("add*", requiredAttribute); // 方法映射配置
        attributeMap.put("edit*", requiredAttribute); // 方法映射配置
        attributeMap.put("delete*", requiredAttribute); // 方法映射配置
        attributeMap.put("get*", readAttribute); // 方法映射配置
        NameMatchTransactionAttributeSource source =
                new NameMatchTransactionAttributeSource(); // 命名匹配事务
        source.setNameMap(attributeMap); // 方法名称的匹配操作的事务控制生效
        TransactionInterceptor interceptor =
                new TransactionInterceptor(transactionManager, source); // 事务拦截器配置
        return interceptor;
    }
    @Bean
    public Advisor transactionAdviceAdvisor(TransactionInterceptor interceptor) {
        String express = "execution (* com.yootk..service.*.*(..))"; // 切面表达式
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); // 切入表达式配置
        pointcut.setExpression(express); // 定义切面
        return new DefaultPointcutAdvisor(pointcut, interceptor);
    }
}


7、
# 【Memcached配置】设置服务组件的地址与端口
memcached.server=memcached-server:6030
# 【Memcached配置】设置服务器的权重
memcached.weights=1
# 【Memcached配置】设置每个缓存服务器的初始化连接数量
memcached.initConn=1
# 【Memcached配置】设置每个缓存服务器的最小维持连接数量
memcached.minConn=1
# 【Memcached配置】设置最大可用的连接数量
memcached.maxConn=50
# 【Memcached配置】配置Nagle算法,因为缓存的处理不需要确认(影响性能)
memcached.nagle=false
# 【Memcached配置】设置Socket读取等待的超时时间
memcached.socketTO=3000
# 【Memcached配置】设置连接池的服务更新时间间隔
memcached.maintSleep=5000

8、
package com.yootk.ssj.config;

import com.whalin.MemCached.MemCachedClient;
import com.whalin.MemCached.SockIOPool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:config/memcached.properties") // 加载资源文件
public class MemcachedConfig {
    @Value("${memcached.server}")
    private String server; // 服务器地址
    @Value("${memcached.weights}")
    private int weight; // 权重
    @Value("${memcached.initConn}")
    private int initConn; // 初始化连接数量
    @Value("${memcached.minConn}")
    private int minConn; // 最小维持的连接数量
    @Value("${memcached.maxConn}")
    private int maxConn; // 最大连接数量
    @Value("${memcached.maintSleep}")
    private int maintSleep; // 连接池维护周期
    @Value("${memcached.nagle}")
    private boolean nagle; // Nagle算法配置
    @Value("${memcached.socketTO}")
    private int socketTO; // 连接超时
    @Bean("socketIOPool")
    public SockIOPool initSocketIOPool() {
        SockIOPool pool = SockIOPool.getInstance("memcachedPool"); // 获取连接池的实例
        pool.setServers(new String[] {this.server}); // 配置服务器地址
        pool.setWeights(new Integer[] {this.weight}); // 配置权重
        pool.setInitConn(this.initConn); // 初始化连接数量
        pool.setMinConn(this.minConn); // 最小连接池数量
        pool.setMaxBusyTime(this.maxConn); // 最大的连接池数量
        pool.setMaintSleep(this.maintSleep); // 维护间隔
        pool.setNagle(this.nagle); // 禁用Nagle算法,以提升处理性能
        pool.setSocketTO(this.socketTO); // 配置连接超时时间
        pool.initialize(); // 初始化连接池
        return pool;
    }
    @Bean
    public MemCachedClient memCachedClient() {
        MemCachedClient cachedClient = new MemCachedClient("memcachedPool");
        return cachedClient;
    }
}


9、
package com.yootk.ssj.config;

import com.yootk.ssj.cache.memcached.MemcachedCacheManager;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new MemcachedCacheManager();
    }
}

10、
package com.yootk.test;

import com.yootk.ssj.cache.memcached.MemcachedCacheManager;
import com.yootk.ssj.context.config.SpringApplicationContextConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ContextConfiguration(classes = {SpringApplicationContextConfig.class})
@ExtendWith(SpringExtension.class)
public class TestMemcached {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestMemcached.class);
    @Autowired
    private MemcachedCacheManager cacheManager;
    @Test
    public void testConnection() throws Exception {
        this.cacheManager.getCache("YootkCache").put("muyan", "muyan-yootk");
        LOGGER.info("【YootkCache】获取Memcached数据:muyan = {}",
                this.cacheManager.getCache("YootkCache").get("muyan"));
    }
}


11、
package com.yootk.ssj.context.config;

import com.yootk.common.interceptor.RequestDataValidateInterceptor;
import com.yootk.common.mapper.CustomObjectMapper;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableWebMvc // 启用SpringMVC
@ComponentScan("com.yootk.ssj.action") // 扫描控制层路径
public class SpringWebContextConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 所有的资源一定要保存在WEB-INF目录之中,那么就必须进行资源映射
        registry.addResourceHandler("/yootk-js/**")
                .addResourceLocations("/WEB-INF/static/js/");
        registry.addResourceHandler("/yootk-css/**")
                .addResourceLocations("/WEB-INF/static/css/");
        registry.addResourceHandler("/yootk-images/**")
                .addResourceLocations("/WEB-INF/static/images/");
        registry.addResourceHandler("/yootk-fonts/**")
                .addResourceLocations("/WEB-INF/static/fonts/"); // 上传资源映射
        // 本次的应用没有引入分布式的文件存储单元,所以直接保村在WEB-INF之中
        registry.addResourceHandler("/yootk-upload/**")
                .addResourceLocations("/WEB-INF/upload/"); // 上传资源映射
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter =
                new MappingJackson2HttpMessageConverter(); // Jackson数据转换器
        CustomObjectMapper objectMapper = new CustomObjectMapper();
        converter.setObjectMapper(objectMapper);
        converter.setSupportedMediaTypes(List.of(
                MediaType.APPLICATION_JSON));
        converters.add(converter); // 追加转换器
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        RequestDataValidateInterceptor interceptor =
                new RequestDataValidateInterceptor(); // 请求数据验证拦截器
        interceptor.setRestSwith(false); // 拦截器显示风格
        registry.addInterceptor(interceptor).addPathPatterns("/pages/**");
    }
}

12、
package com.yootk.ssj.action.advice;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler
    public ModelAndView handle(Exception e, HttpServletRequest request,
                               HttpServletResponse response) {
        ModelAndView mav = new ModelAndView("forward:/error"); // 跳转到Action上
        Map<String, String> result = new HashMap<>();
        result.put("message", e.getMessage());
        result.put("type", e.getClass().getName());
        result.put("path", request.getRequestURI());
        result.put("referer", request.getHeader("Referer"));
        mav.addObject("errors", result); // 保存错误信息
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 500状态码
        return mav;
    }
}

13、
package com.yootk.ssj.action.back;

import com.yootk.common.web.action.abs.AbstractAction;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class BackIndexAction extends AbstractAction {
    @RequestMapping("/pages/back")
    public String index() {
        return "/back/index";
    }
}


14、
localhost/pages/back

图书分类列表

图书分类列表

  • 图书数据的分类列表信息在项目应用过程中一般改动较少,同时在图书数据进行增加或修改时都需要加载分类数据,那么此时可以将分类信息保存在缓存数据库之中

数据分类列表操作流程

  • 在本次的项目开发中,通过应用程序的调用实现缓存数据处理,即:开发者第一次显示分类列表时将通过 SQL 数据库查询结果,而后会将该结果保存在缓存之中,这样第二次再进行分类信息列表展示或者其他地方需要进行分类数据加载时,就可以通过缓存实现快速加载
1、
package com.yootk.ssj.po;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

import java.io.Serializable;

@Entity
public class Item implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键生成
    private Long iid;
    private String name;
    private String note;
    public Long getIid() {
        return iid;
    }
    public void setIid(Long iid) {
        this.iid = iid;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getNote() {
        return note;
    }
    public void setNote(String note) {
        this.note = note;
    }
}


2、

package com.yootk.ssj.dao;

import com.yootk.ssj.po.Item;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IItemDAO extends JpaRepository<Item, Long> {}

3、

package com.yootk.ssj.service;

import com.yootk.ssj.po.Item;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;

import java.util.List;

@CacheConfig(cacheNames = "itemCache") // 配置缓存名称
public interface IItemService {
    @Cacheable("items")
    public List<Item> list();
}

4、
package com.yootk.ssj.service.abs;

public abstract class AbstractService {
}


5、
package com.yootk.ssj.service.impl;

import com.yootk.ssj.dao.IItemDAO;
import com.yootk.ssj.po.Item;
import com.yootk.ssj.service.IItemService;
import com.yootk.ssj.service.abs.AbstractService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ItemServiceImpl extends AbstractService implements IItemService {
    @Autowired
    private IItemDAO itemDAO;
    @Override
    public List<Item> list() {
        return this.itemDAO.findAll(); // 查询全部数据
    }
}


6、
package com.yootk.test;

import com.yootk.ssj.context.config.SpringApplicationContextConfig;
import com.yootk.ssj.service.IItemService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.concurrent.TimeUnit;

@ContextConfiguration(classes = {SpringApplicationContextConfig.class})
@ExtendWith(SpringExtension.class)
public class TestItemService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestItemService.class);
    @Autowired
    private IItemService itemService; // 业务接口实例
    @Test
    public void testConnection() throws Exception {
        LOGGER.info("【第1次查询分类信息】{}", this.itemService.list());
        TimeUnit.SECONDS.sleep(2); // 等待2秒后继续查询
        LOGGER.info("【第2次查询分类信息】{}", this.itemService.list());
    }
}


7、
telnet memcached-server 6030

8、
package com.yootk.ssj.action.back;

import com.yootk.ssj.po.Item;
import com.yootk.ssj.service.IItemService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/pages/back/admin/item")
public class ItemAction {
    private static final Logger LOGGER = LoggerFactory.getLogger(ItemAction.class);
    @Autowired
    private IItemService itemService;
    @RequestMapping("list")
    public ModelAndView list() {
        ModelAndView mav = new ModelAndView("/back/admin/item/item_list");
        List<Item> allItems = this.itemService.list(); // 直接业务查询
        LOGGER.debug("【查询全部分类】{}", allItems);
        mav.addObject("allItems", allItems);
        return mav; // 页面跳转
    }
}

强制刷新缓存

强制刷新缓存数据

  • 分类数据信息保存在 Memcached 缓存之后,在每次进行列表展示时,可以通过缓存进行加载,这样就可以提升页面的响应速度,但是随之而来的问题就在于,当分类数据更新时,无法实时的获取更新后的分类项,所以就需要进行缓存的强制性的刷新处理
1、
package com.yootk.ssj.service;

import com.yootk.ssj.po.Item;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;

import java.util.List;

@CacheConfig(cacheNames = "itemCache") // 配置缓存名称
public interface IItemService {
    @Cacheable("items")
    public List<Item> list();
    @CacheEvict("items") // 清空缓存数据
    public default void clear() {}
}


2、
package com.yootk.ssj.action.back;

import com.yootk.ssj.po.Item;
import com.yootk.ssj.service.IItemService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/pages/back/admin/item")
public class ItemAction {
    private static final Logger LOGGER = LoggerFactory.getLogger(ItemAction.class);
    @Autowired
    private IItemService itemService;
    @RequestMapping("list")
    public ModelAndView list() {
        ModelAndView mav = new ModelAndView("/back/admin/item/item_list");
        List<Item> allItems = this.itemService.list(); // 直接业务查询
        LOGGER.debug("【查询全部分类】{}", allItems);
        mav.addObject("allItems", allItems);
        return mav; // 页面跳转
    }
    @RequestMapping("refresh")
    public String refresh() {
        LOGGER.debug("强制性刷新缓存");
        this.itemService.clear(); // 清除缓存
        return "forward:/pages/back/admin/item/list"; // 重定向到其他控制层方法
    }
}

分类数据增加业务

增加图书分类项

  • 图书分类数据基于数据库存储,主要是为了方便使用者进行数据的动态维护,而在动态维护的过程中,就需要对外提供数据增加表单,考虑到数据有效性的问题,需要在前端表单以及后端请求接收时进行数据验证处理,验证通过后才可以进行数据库更新操作
1、
package com.yootk.ssj.service;

import com.yootk.ssj.po.Item;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;

import java.util.List;

@CacheConfig(cacheNames = "itemCache") // 配置缓存名称
public interface IItemService {
    @Cacheable("items")
    public List<Item> list();
    @CacheEvict("items") // 清空缓存数据
    public default void clear() {}
    public boolean add(Item item); // 数据增加业务
}


2、
   @Override
    public boolean add(Item item) {
        // 当前分类表之中的ID采用了自动增长的方式实现处理,所以增加完成之后,可以直接返回当前的增加后的ID
        return this.itemDAO.save(item).getIid() != null;
    }

3、
    @ErrorPage
    @RequestDataValidate("name:string;note:string") // 定义拦截验证规则
    @RequestMapping("add")
    public ModelAndView add(Item item) {
        LOGGER.info("【PO】{}", item);
        ModelAndView mav = new ModelAndView("/plugin/forward"); // 公共的成功提示页
        if (this.itemService.add(item)) {   // 业务调用成功
            mav.addObject("msg", "分类信息增加成功!");
        } else {
            mav.addObject("msg", "分类信息增加失败!");
        }
        mav.addObject("url", "/pages/back/admin/item/add_input"); // 返回路径
        return mav;
    }

图书数据分页列表

数据分页显示

  • 图书信息数据维护本次实现 CRUD 操作的核心结构在实际的应用中,会存在有多条图书信息,所以应该考虑以分页的形式加载所需数据并且在进行信息列表时也需要考虑数据库数据的检索需要
1、
package com.yootk.common.util;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class PageUtil { // 分页控制处理操作
    private Integer currentPage = 1; // 默认的首页页码
    private Integer lineSize = 2; // 每页显示2条记录
    private String column; // 模糊查询列
    private String keyword; // 模糊查询关键字
    private String columnData; // 模糊查询候选列
    // 分页之中所有的请求处理以及属性到页面的传递都需要此对象的支持
    private HttpServletRequest request; // 获取用户的请求数据
    private String url; // 分页的处理路径
    public PageUtil(String url) {
        this(url, null); // 不需要查询操作的支持
    }
    public PageUtil(String url, String columnData) { // 接收初始化参数
        this.url = url;
        this.column = columnData;
        this.request = ((ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes()).getRequest();
        this.splitHandler(); // 进行分页参数的处理
    }
    private void splitHandler() {
        // 所有分页参数在发送的时候都是以请求参数的形式发送过来的,所以直接接收即可
        try { // 表示当前所在的页码
            this.currentPage = Integer.parseInt(this.request.getParameter("cp"));
        } catch (Exception e) {}
        try { // 当前每页显示的行数
            this.lineSize = Integer.parseInt(this.request.getParameter("ls"));
        } catch (Exception e) {}
        // 按照正常的架构设计,此时的信息不应该通过数据库查询,而是应该通过专属的索引数据库查询
        this.column = this.request.getParameter("col"); // 模糊查询列
        this.keyword = this.request.getParameter("kw"); // 模糊查询关键字
        // 考虑到后续的页面需要进行分页数据的展示,那么要进行属性的传递
        this.request.setAttribute("currentPage", this.currentPage);
        this.request.setAttribute("lineSize", this.lineSize);
        this.request.setAttribute("column", this.column);
        this.request.setAttribute("keyword", this.keyword);
        this.request.setAttribute("url", this.url);
        this.request.setAttribute("columnData", this.columnData);
    }
    public Integer getCurrentPage() {
        return currentPage;
    }
    public Integer getLineSize() {
        return lineSize;
    }
    public String getColumn() {
        return column;
    }
    public String getKeyword() {
        return keyword;
    }
    // 此时可以根据需要定义Setter、Getter的处理方法,但是本次只定义了四个获取操作
}


2、
package com.yootk.ssj.service;

import java.util.Map;

public interface IBookService {
    /**
     * 实现数据的分页展示操作
     * @param currentPage 当前所在的页码
     * @param lineSize 每页显示的数据行数
     * @param column 模糊查询列
     * @param keyword 模糊查询字段
     * @return 包含当前所加载的全部数据的集合以及总行数记录
     * key = allData、value = 全部图书信息集合(List集合)
     * key = allRecorders、value = 数据匹配的行数(COUNT()函数统计结果)
     * key = allPages、value = 总页数
     * key = allItem、value = 全部的分类数据信息(Map集合),用于显示分类的名称
     */
    public Map<String, Object> list(int currentPage, int lineSize,
                                    String column, String keyword);
}


3、
package com.yootk.ssj.service.abs;

import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public abstract class AbstractService {
    /**
     * 表示进行空字符串的判断,里面只要存在有一个空字符串就返回false
     * @param data 要查询的数据项
     * @return 如果有一个数据为空则返回false,全部不为空的时候返回true
     */
    public boolean checkEmpty(String ... data) {
        for (String str : data) {
            if (!StringUtils.hasLength(str)) { // 没有长度
                return false;
            }
        }
        return true;
    }

    /**
     * 通过反射设置相关的对象实例内容
     * @param object 当前操作的对象
     * @param name 属性名称
     * @param value 属性的内容
     */
    public void setObjectProperty(Object object, String name, Object value) {
        try {
            Class<?> clazz = object.getClass();
            Field field = clazz.getDeclaredField(name); // 获取专属的属性内容
            Method method = clazz.getMethod("set" + StringUtils.capitalize(name), field.getType());
            method.invoke(object, value);
        } catch (Exception e) {}
    }
}



4、
package com.yootk.ssj.po;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键生成
    private Long bid;
    private String name;
    private String author;
    private Integer price; // 单位:分
    private String cover; // 图书封面
    private String note; // 图书简介

    public Long getBid() {
        return bid;
    }

    public void setBid(Long bid) {
        this.bid = bid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public String getCover() {
        return cover;
    }

    public void setCover(String cover) {
        this.cover = cover;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}


5、
package com.yootk.ssj.dao;

import com.yootk.ssj.po.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IBookDAO extends JpaRepository<Book, Long> {
}


6、
package com.yootk.ssj.service.impl;

import com.yootk.ssj.dao.IBookDAO;
import com.yootk.ssj.po.Book;
import com.yootk.ssj.service.IBookService;
import com.yootk.ssj.service.IItemService;
import com.yootk.ssj.service.abs.AbstractService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
@Service
public class BookServiceImpl extends AbstractService implements IBookService {
    // 如果此时注入的是IItemDAO接口,那么意味着在进行图书列表查询的时候还要查询数据库
    @Autowired
    private IItemService itemService; // 注入业务层
    @Autowired
    private IBookDAO bookDAO; // 图书表的数据操作
    @Override
    public Map<String, Object> list(int currentPage, int lineSize, String column, String keyword) {
        Map<String, Object> result = new HashMap<>(); // 保存存储结果
        // 如果要想在SpringDataJPA里面进行数据分页的控制操作,那么需要使用专属的工具类
        Pageable pageable = PageRequest.of(currentPage - 1, lineSize); // 分页定义
        Page<Book> page = null; // 保存查询的结果
        if (super.checkEmpty(column, keyword)) { // 检查是否需要进行模糊查询
            // 在使用SpringDataJPA操作的时候,往往要通过具体的PO实例来进行数据的配置
            Book book = new Book(); // 实例化PO对象
            super.setObjectProperty(book, column, keyword); // 属性设置
            ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                    .withMatcher(column, matcher -> matcher.contains()); // 包含指定的内容
            Example<Book> example = Example.of(book, exampleMatcher); // 准备模糊查询
            page = this.bookDAO.findAll(example, pageable);
        } else {    // 是普通的分页操作
            page = this.bookDAO.findAll(pageable);
        }
        result.put("allData", page.getContent()); // 获取图书信息
        result.put("allRecorders", page.getTotalElements()); // 总行数
        result.put("allPages", page.getTotalPages()); // 总页数
        // 还需要在图书列表的时候展示其对应的分类的名称信息
        Map<Long, String> items = new HashMap<>(); // 保存分类数据
        this.itemService.list().forEach((item)->{
            items.put(item.getIid(), item.getName());
        });
        result.put("allItems", items);
        return result;
    }
}


7、
package com.yootk.test;

import com.yootk.ssj.context.config.SpringApplicationContextConfig;
import com.yootk.ssj.service.IBookService;
import com.yootk.ssj.service.IItemService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.concurrent.TimeUnit;

@ContextConfiguration(classes = {SpringApplicationContextConfig.class})
@ExtendWith(SpringExtension.class)
public class TestBookService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBookService.class);
    @Autowired
    private IBookService bookService; // 业务接口实例
    @Test
    public void testList() throws Exception {
        LOGGER.info("{}", this.bookService.list(1, 5, "", ""));
    }
}


8、
package com.yootk.ssj.action;

import com.yootk.common.util.PageUtil;
import com.yootk.ssj.service.IBookService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/pages/back/admin/book")
public class BookAction {
    private static final Logger LOGGER = LoggerFactory.getLogger(BookAction.class);
    @Autowired
    private IBookService bookService;
    @RequestMapping("list")
    public ModelAndView list() {
        PageUtil pageUtil = new PageUtil("/pages/back/admin/book/list",
                "图书名称name|图书作者:author"); // 设置分页操作
        ModelAndView mav = new ModelAndView("/back/admin/book/book_list"); // 前端页面
        mav.addAllObjects(this.bookService.list(pageUtil.getCurrentPage(),
                pageUtil.getLineSize(), pageUtil.getColumn(), pageUtil.getKeyword()));
        return mav;
    }
}


增加图书数据

图书数据增加表单处理流程

  • 每一项图书数据,都需要有与之对应的图书分类,由于图书分类的数据属在本应用中,于动态维护,所以在进行图书表单信息填写之前,首先要对图书分类数据进行加载,并将其填充到下拉列表框之中,由于一本图书只对应有一个图书分类,所以此时的下拉列表框的长度应设置为 1

图书数据保存

  • 在进行表单数据填写时,所填写的价格(参数名称为:tprice)为小数而为了保证应用的安全则需要在数据保存时将其转为整型(货币单位:分)进行存储而对于上传的也应该将其自动命名后存储到相应的目录之中图片(参数名称为:photo)
1、
package com.yootk.common.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

public class UploadFileUtils { // 上传工具类
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadFileUtils.class);
    public static final String NO_PHOTO = "nophoto.png"; // 默认文件名称
    private UploadFileUtils() {} // 构造方法隐藏

    /**
     * 上传文件存储处理,在文件保存的时候会根据UUID生成新的文件名称,并保存在指定的目录之中
     * @param file 待上传的文件
     * @param saveDir 保存的目录
     * @return 生成的文件名称
     */
    public static String save(MultipartFile file, String saveDir) {
        if (file == null || file.getSize() == 0) {  // 没有上传文件
            return NO_PHOTO; // 返回默认的文件名称
        }
        String fileName = UUID.randomUUID() + "." + file.getContentType()
                .substring(file.getContentType().lastIndexOf("/") + 1); // 创建文件名称
        LOGGER.debug("生成文件名称:{}", fileName);
        save(file, saveDir, fileName);
        return fileName;
    }
    public static boolean save(MultipartFile file, String saveDir, String fileName) {
        String filePath = ContextLoader.getCurrentWebApplicationContext()
                .getServletContext().getRealPath(saveDir) + File.separator + fileName;
        LOGGER.info("文件存储路径:{}", filePath);
        try {
            file.transferTo(new File(filePath));
        } catch (IOException e) {
            LOGGER.error("文件存储失败:{}", e.getMessage());
            return false;
        }
        return true;
    }
    public static boolean delete(String saveDir, String fileName) {
        if (!StringUtils.hasLength(fileName)) { // 文件名称不存在
            return true; // 文件不存在直接返回成功
        }
        if (NO_PHOTO.equals(fileName)) {    // 默认的文件名称
            return true; // 默认文件不删除
        }
        String filePath = ContextLoader.getCurrentWebApplicationContext()
                .getServletContext().getRealPath(saveDir) + File.separator + fileName;
        File file = new File(filePath);
        if (file.exists()) { // 文件存在
            return file.delete(); // 删除文件
        }
        return true;
    }
}


2、
    /**
     * 执行数据增加前的处理操作
     * @return 返回图书增加前所需要的内容项
     * key = allItems、value = 图书分类集合(List)
     */
    public Map<String, Object> preAdd();
    public boolean add(Book book); // 数据增加

3、
    @Override
    public Map<String, Object> preAdd() {
        Map<String, Object> result = new HashMap<>();
        result.put("allItems", this.itemService.list()); // 直接获取缓存数据
        return result;
    }

    @Override
    public boolean add(Book book) {
        return this.bookDAO.save(book).getBid() != null;
    }

4、
    @Autowired
    private IBookService bookService;
    private static final String SAVE_DIR = "/WEB-INF/upload/book/"; // 文件保存路径
    @RequestMapping("add_input")
    public ModelAndView addInput() {
        ModelAndView mav = new ModelAndView("/back/admin/book/book_add");
        mav.addAllObjects(this.bookService.preAdd()); // 获取全部的分类数据
        return mav;
    }
    @ErrorPage
    @RequestDataValidate("name:string;note:string;tprice:double;iid:long;photo:upload")
    @PostMapping("add")
    public ModelAndView add(Book book, double tprice,
                            @UploadFileType MultipartFile photo) {
        book.setCover(UploadFileUtils.save(photo, SAVE_DIR)); // 保存上传文件名称
        book.setPrice((int)(tprice * 100)); // 按照“分”单位保存数据
        ModelAndView mav = new ModelAndView("/plugin/forward"); // 跳转路径
        if (this.bookService.add(book)) {
            mav.addObject("msg", "图书信息增加成功!");
        } else {
            mav.addObject("msg", "图书信息增加失败");
        }
        mav.addObject("url", "/pages/back/admin/book/add_input");
        return mav;
    }

显示图书详情

图书详情

  • 在图书列表页面中,除了可以实现数据的分页显示之外,还提供有“查看详情”的提示信息,用户只需要单击此文字就可以通过 Ajax 实现图书详情的异步加载。为了便于图书详情的浏览,本次会通过 Bootstrap 组件提供的模态窗口进行数据显示,考虑到传输性能的问题以及 DOM 处理操作的方便,此时控制端需要以 JSON 的数据形式返回查询结果
1、
    /**
     * 查询指定图书编号的详情,此操作会返回图书信息以及对应的分类信息
     * @param bid 要查询的图书编号
     * @return 图书的详情,包括如下数据项:
     * key = book、value = 图书信息
     * key = item、value = 图书对应的分类详情
     */
    public Map<String, Object> get(long bid); // 根据ID加载图书信息

2、
    @Autowired
    private IItemDAO itemDAO;
    @Override
    public Map<String, Object> get(long bid) {
        Map<String, Object> result = new HashMap<>();
        Book book = this.bookDAO.findById(bid).get(); // 获取图书信息
        result.put("item", this.itemDAO.findById(book.getIid()).get()); // 图书分类信息
        result.put("book", book); // 保存图书信息
        return result;
    }

3、
    @ResponseBody
    @GetMapping("get")
    public Object get(long bid) {
        return this.bookService.get(bid); // 直接响应结果
    }

4、
$(function() { // jQuery页面加载处理
	$("span[id^=bid-]").each(function(){ // 获取指定ID开头的元素
		$(this).on("click",function(){ // 绑定单击事件
			bid = this.id.split("-")[1] ; // 获取图书编号
			$.get("/pages/back/admin/book/get",{ bid : bid }, function(data){ // Ajax请求
			    $(bookCover).attr("src", "/yootk-upload/book/" + data.book.cover) // 图片设置
			    $(bookName).text(data.book.name) // 普通文本设置
			    $(bookAuthor).text(data.book.author) // 普通文本设置
			    $(bookNote).html(data.book.note) // HTML文本设置
			    $(bookPrice).text(data.book.price / 100) // 普通文本设置
			    $(bookItem).text(data.item.name) // 普通文本设置
                $("#bookInfo").modal("toggle") ; // 显示模态窗口
            },"json"); // 响应类型为JSON
		}) ;
	}) ;
})

修改图书数据

图书信息表单回填处理

  • 图书列表页面提供了数据的修改链接,在进行图书数据修改时,需要向控制层传递图书 ID,而后通过图书 ID 获取对应的图书信息以及全部图书分类数据,并将其回填到 book_edit.isp 页面提供的表单之中

更新图书数据

  • 当用户进入到 book_edit.jsp 图书编辑页面时,需要在页面上显示出已有的图书信息图书封面照片、对应的图书分类,考虑到数据处理的需要,还应该增加表单隐藏域,用于提交当前要修改的图书编号以及原始图片名称,原始图片名称主要用于实现图书封面照片的更新使用
1、
    /**
     * 数据更新前的查询操作,该操作会根据图书ID获取图书详情,以及所有分类的信息
     * @param bid 要修改的图书ID
     * @return 返回图书修改前所需的数据内容
     * key = allItems、value = 图书分类集合(List)
     * key = book、value = 图书数据(Book)
     */
    public Map<String, Object> preEdit(long bid);

    /**
     * 图书信息修改操作
     * @param book 要修改的数据信息
     * @return 更新成功返回true,否则返回false
     */
    public boolean edit(Book book);

2、
    @Override
    public Map<String, Object> preEdit(long bid) {
        Map<String, Object> result = new HashMap<>();
        result.put("allItems", this.itemService.list()); // 直接获取缓存数据
        result.put("book", this.bookDAO.findById(bid).get());
        return result;
    }

    @Override
    public boolean edit(Book book) {
        return this.bookDAO.save(book).getBid() != null;
    }

3、
    @RequestMapping("edit_input")
    public ModelAndView editInput(long bid) { // 修改前的表单回填
        ModelAndView mav = new ModelAndView("/back/admin/book/book_edit");
        mav.addAllObjects(this.bookService.preEdit(bid));
        return mav;
    }
    @ErrorPage
    @RequestDataValidate("bid:long;name:string;note:string;" +
            "tprice:double;iid:long;photo:upload")
    @PostMapping("edit")
    public ModelAndView edit(Book book, double tprice,
                             @UploadFileType MultipartFile photo) {
        // Book表中使用cover字段描述封面信息,这个cover将传输原始的图片名称
        book.setPrice((int)(tprice * 100)); // 保存单位
        if (photo != null && photo.getSize() > 0) { // 有新的文件上传
            if (UploadFileUtils.NO_PHOTO.equals(book.getCover())) { // 原始没有图片名称
                book.setCover(UploadFileUtils.save(photo, SAVE_DIR)); // 上传并生成新的文件名称
            } else {    // 如果有已上传文件名称
                UploadFileUtils.save(photo, SAVE_DIR, book.getCover());
            }
        }
        ModelAndView mav = new ModelAndView("/plugin/forward");
        if (this.bookService.edit(book)) { // 业务调用
            mav.addObject("msg", "图书信息修改成功!");
            mav.addObject("msg", "图书信息修改失败!");
        }
        mav.addObject("url", "/pages/back/admin/book/list");
        return mav;
    }

删除图书数据

删除图书数据

  • 当某一本图书的数据不再需要时,可以通过列表页面提供的“删除图书”的链接进行图书数据的删除,由于每一本图书都可能对应有一个完整的封面,所以在删除时应该将图书以及对应的图片文件一并删除
1、
    /**
     * 删除图书信息
     * @param bid 要删除的图书编号
     * @return 删除图书的对应的图片名称
     */
    public String delete(long bid);

2、
    @Override
    public String delete(long bid) {
        Optional<Book> optionalBook = this.bookDAO.findById(bid);
        if (optionalBook.isPresent()) {    // 数据是否存在
            Book book = optionalBook.get(); // 获取图书信息
            this.bookDAO.delete(book); // 删除对象实体
            return book.getCover(); // 获取图书对应的图像
        }
        return null;
    }

3、

    @ErrorPage
    @RequestDataValidate("bid:long")
    @GetMapping("delete")
    public ModelAndView delete(long bid) {
        ModelAndView mav = new ModelAndView("/plugin/forward");
        String cover = this.bookService.delete(bid); // 删除数据
        if (StringUtils.hasLength(cover)) { // 文件名称存在
            UploadFileUtils.delete(SAVE_DIR, cover); // 直接删除文件内容
        }
        mav.addObject("msg", "图书信息删除成功!");
        mav.addObject("url", "/pages/back/admin/book/list");
        return mav;
    }

demo


上次编辑于: