原创

基于JavaSSM框架的开发项目

基于JavaSSM框架的开发项目

ssm整合思路

父工程pom.xml文件依赖配置清单,整个项目的依赖全部从父工程配置,子工程直接继承即可

<!--版本声明-->
<properties>
    <spring.version>4.3.20.RELEASE</spring.version>
    <spring.security>4.2.10.RELEASE</spring.security>
</properties>

<dependencies>
    <!--spring依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <!--移除spring自带的日志分析,启用slf4j-->
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!--spring AOP 相关-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.2</version>
    </dependency>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>2.2</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.21</version>
    </dependency>
    <!--数据源-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.2.8</version>
    </dependency>
    <!--mybatis和spring整合-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.2.2</version>
    </dependency>
    <!--mybatis分页-->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>4.0.0</version>
    </dependency>
    <!--日志-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.7</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!--其他日志框架的中转包-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>1.7.25</version>
    </dependency>
    <!--json转换依赖-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.8</version>
    </dependency>
    <!--JSTL标签库-->
    <dependency>
        <groupId>jstl</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <!--junit测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <!--servlet容器相关依赖-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
    <!--JSP页面使用的依赖-->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.1.3-b06</version>
        <scope>provided</scope>
    </dependency>

    <!--自身需要json数据转换的依赖-->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.8.5</version>
    </dependency>
    <!--springSecurity对web应用进行权限管理-->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${spring.security}</version>
    </dependency>
    <!--spring security配置-->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${spring.security}</version>
    </dependency>
    <!--spring security标签库-->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring.security}</version>
    </dependency>
</dependencies>

spring整合mybatis框架

# jdbc.properties 数据库信息配置文件

jdbc.user=root
jdbc.password=runner-liu.1214
jdbc.url=jdbc:mysql://localhost:3306/project_crowd?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
jdbc.driver=com.mysql.cj.jdbc.Driver
<!-- mybatis-config.xml	Mybatis主配置文件,不定义内容,实际就是个空头文件 -->

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

</configuration>
<!-- spring-persist-mybatis.xml	spring整合Mybatis的配置文件-->

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--加载外部的属性文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
    </bean>

    <!--配置SqlSessionFactoryBean工厂整合Mybatis-->
    <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--指定mybatis全局文件的位置-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--配置Mapper.xml配置文件的位置-->
        <property name="mapperLocations" value="classpath:mybatis/mapper/*Mapper.xml"/>
        <!--配置数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置MapperScannerConfigurer,将mapper加入IOC容器中,启用注解支持-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.crowdfunding.mapper"/>
    </bean>
</beans>
<!-- spring-persist-tx.xml	spring配置事务以及AOP-->

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置注解扫描,主要是为了将Service注解扫描到IOC容器中-->
    <context:component-scan base-package="com.crowdfunding.service"/>

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--
			装配数据源,此时报错是因为在此文件中找不到dataSource的相关Bean,
			只要运行时一同加载了dataSource所在的配置文件,就能在IOC容器中找到
			-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!--配置事务的属性-->
        <tx:attributes>
            <!--查询方法配置只读属性,让数据库根据查询操作进行一些优化-->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="count*" read-only="true"/>

            <!--增删改的方法配置事务的传播行为和回滚的异常,
                propagation="REQUIRED":当前方法必须工作在事务上.
						如果当前线程上有开启的事务,就使用开启的事务,没有就自己开一个事务.
						此方法不宜使用,用别人的事务有可能被回滚.
                propagation="REQUIRED_NEW":当前方法必须工作在事务上.
						不管当前线程上有没有事务,都开一个新的事务供自己运行,
						此方法适合使用,不会受到其他事务的影响.
                rollback-for="java.lang.Exception":默认运行时异常(RunnerTimeException)回滚.
						我们的要求是不管什么异常都会滚,包括编译时异常
            -->
            <tx:method name="save*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception"/>
            <tx:method name="remove*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception"/>
            <tx:method name="update*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception"/>
            <tx:method name="batch*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception"/>
        </tx:attributes>
    </tx:advice>
    <!--配置事务的切面-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="txPointcut" expression="execution(* *..*ServiceImpl.*(..))"/>
        <!--将切入点表达式和事务通知关联起来-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
</beans>

spring整合springmvc框架

<!-- web.xml -->

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>

    <!--初始化servlet前加载spring以及整合mybatis的配置文件-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-persist-*.xml</param-value>
    </context-param>


    <!--配置filter全局过滤设置字符集为utf-8-->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--初始化参数设置字符集为UTF-8-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!--强制请求及响应设置字符集-->
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <!--字符集的filter必须在所有的filter之前-->
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <!--启用监听器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>


    <!--配置springMVC控制器-->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--初始化时加载springMVC的配置文件-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-web-mvc.xml</param-value>
        </init-param>
        <!--服务器启动时创建要给servlet (servlet默认情况下是在第一次请求时创建,
        但是在服务器启动时我们需要初始化框架的一些配置,所以需要跟随服务器一起启动)-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <!--第一种配置方式表示拦截所有的请求,并在springMVC的配置文件中设置静态资源路径-->
        <url-pattern>/</url-pattern>
        <!--第二种方式可以通过扩展名配置拦截
            优点:静态资源不会设置拦截;
					再者可以实现伪静态的效果,表面上是访问一个静态的HTML文件,
					但是实际上是经过filter处理的
            缺点:不符合Restful风格,Restful风格提倡不写文件扩展名
			   伪静态的作用:1、给黑客入侵增加难度;
							2、有利于SEO的优化,让搜索引擎更容易找到我们的项目。
        -->
        <!--<url-pattern>*.html</url-pattern>-->
        <!--还得定义一个*.json的扩展名
            如果要给Ajax请求扩展名为html,但是实际服务器返回的是json数据,就会出现406错误码。
            所以为了让Ajax请求能够顺利的拿到响应数据,需要另外配置一个*.json扩展名
        -->
        <!--<url-pattern>*.json</url-pattern>-->
    </servlet-mapping>
</web-app>
<!-- spring-web-mvc.xml -->

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
    <!--扫描注解到IOC容器-->
    <context:component-scan base-package="com.crowdfunding.mvc"/>

    <!--配置springMVC的注解驱动-->
    <mvc:annotation-driven/>

    <!--配置视图解析器-->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--配置静态资源路径,跳过filter的拦截-->
    <mvc:resources mapping="/bootstrap/**" location="/bootstrap/"/>
    <mvc:resources mapping="/crowd/**" location="/crowd/"/>
    <mvc:resources mapping="/css/**" location="/css/"/>
    <mvc:resources mapping="/fonts/**" location="/fonts/"/>
    <mvc:resources mapping="/img/**" location="/img/"/>
    <mvc:resources mapping="/jquery/**" location="/jquery/"/>
    <mvc:resources mapping="/layer/**" location="/layer/"/>
    <mvc:resources mapping="/script/**" location="/script/"/>
    <mvc:resources mapping="/ztree/**" location="/ztree/"/>

    <!--基于xml的异常映射, 在未配置基于注解的配置之前,单独配置xml也会接管基于注解的异常处理-->
    <bean id="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <!--key 属性指定异常全类名,标签体中写对应的视图(这个值使用时是要拼前后缀得到具体路径的)-->
                <prop key="java.lang.Exception">system-error</prop>
            </props>
        </property>
    </bean>

    <!--
        配置view-controller 作用是直接把请求地址和视图名称关联起来,就不必写handler方法了
        换个说法,凡是不需要传参的请求基本上都可以写到这里
    -->
    <mvc:view-controller path="/admin/to/login/page" view-name="admin-login"/>
    <mvc:view-controller path="/admin/to/main/page" view-name="admin-main"/>
    
    <!--注册拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--配置需要拦截的资源 /* 对应一层路径 /**对应多层路径-->
            <mvc:mapping path="/**"/>
            <!--配置不需要拦截的路径-->
            <mvc:exclude-mapping path=""/>
            <!--配置拦截器的类-->
            <bean class=""/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

统一Ajax请求格式

在util模块下创建一个类,用于统一全局Ajax请求返回格式

// ResultEntity.java

package com.crowdfunding;

/**
 * 定义一个工具类用于统一Ajax请求数据的返回结果,同样可以用于分布式架构各模块间调用返回统一类型
 */
public class ResultEntity<T> {
    private String result; // 用于封装当前的处理结果是成功还是失败
    private String message; // 用于请求处理失败后需要返回的信息
    private T data; // 用于请求成功后需要返回的数据

    private static final String FAILED = "failed";
    public static final String SUCCESS = "success";

    /**
     * 配置一个静态方法,用于表示请求成功但是不需要返回数据
     * @return ResultEntity<E>
     */
    public static <E> ResultEntity<E> successWithoutData() {
        return new ResultEntity<E>(SUCCESS, null, null);
    }

    /**
     * 请求处理成功且需要返回数据
     * @param data 要返回的数据
     * @return ResultEntity<E>
     */
    public static <E> ResultEntity<E> successWithData(E data) {
        return new ResultEntity<>(SUCCESS, null, data);
    }

    /**
     * 请求处理失败后使用的方法
     * @param message 失败的错误消息
     * @return ResultEntity<E>
     */
    public static <E> ResultEntity<E> failedWithMessage(String  message) {
        return new ResultEntity<E>(FAILED, message, null);
    }

    public ResultEntity() {
    }

    public ResultEntity(String result, String message, T data) {
        this.result = result;
        this.message = message;
        this.data = data;
    }

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "ResultEntity{" +
                "result='" + result + '\'' +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }
}

配置全局常量类

在util模块下创建一个常量类,用于 统一定义管理全局常量

package com.crowdfunding;

/**
 * 项目的常量类,常量类的作用就是将一些需要传比如字符串之类的参数定义为常量,
 * 避免在多处引用时因为手打打错而造成不必要的错误
 * 再有就是将一些消息提示类信息定义到这里方便统一管理。
 */
public class CrowdConstant {

    public static final String MESSAGE_STRING_INVALIDATE = "字符串不合法,请不要传入空的字符串";
    public static final String MESSAGE_LOGIN_FAILED = "账号密码错误,请重新输入!";
    public static final String ATTR_NAME_EXCEPTION = "exception";
    public static final String ATTR_NAME_LOGIN_ADMIN = "loginAdmin";
    public static final String MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE = "系统内数据异常,登录账号不唯一";
    public static final String MESSAGE_ACCESS_FORBIDDEN = "未登录状态无访问权限";
}

配置工具类

在util模块下创建一个工具类,用于配置项目中使用到的工具比如MD5算法加密,请求类型判断等

package com.crowdfunding;

import javax.servlet.http.HttpServletRequest;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 通用工具类
 */
public class CrowdUtils {
    /**
     * 用于判断前端过来的请求是普通请求还是Ajax请求
     *
     * @param request 请求
     * @return true:当前请求是ajax,否则不是。
     */
    public static boolean judgeRequestType(HttpServletRequest request) {
        // 获取请求消息头
        final String header = request.getHeader("Accept");
        final String XRequestHeader = request.getHeader("X-Requested-With");
        return (header != null && header.contains("application/json")) || (XRequestHeader != null && XRequestHeader.equals("XMLHttpRequest"));
    }

    /**
     * md5加密算法
     * @param sourceStr
     * @return
     */
    public static String md5(String sourceStr) {
        // 判断源字符串(sourceStr是不是有效的)
        if (sourceStr == null || sourceStr.length() == 0) {
            // 如果不是有效的字符串,抛出异常
            throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);
        }
        // 获取MessageDigest对象
        String algorithm = "md5";
        try {
            final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            //获取明文字符串对应的字节数组
            final byte[] sourceStrBytes = sourceStr.getBytes();
            // 加密
            final byte[] digest = messageDigest.digest(sourceStrBytes);
            // 创建BigInteger对象,将加密后的数组转化为一个正大整数 signum = 1表示正数 -1表示负数 0就是0
            int signum = 1;
            final BigInteger bigInteger = new BigInteger(signum, digest);

            // 按照16进制将BigInteger的值转化为字符串并转化转化为大写
            int radix = 16;
            return bigInteger.toString(radix).toUpperCase();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
}

异常处理机制的使用方法

自定义异常类的编写实际需要在开发过程中需要的时候再编写,这里为了和异常捕获串联起来举个例子

自定义登录异常类

工具模块中定义一个 LoginFailedException.java 用于定义登录异常

package com.crowdfunding.exception;

/**
 * 自定义异常类--登录失败后抛出的异常
 */
public class LoginFailedException extends RuntimeException{
    //默认加载所有父类的构造器即可
    public LoginFailedException() {
    }

    public LoginFailedException(String message) {
        super(message);
    }

    public LoginFailedException(String message, Throwable cause) {
        super(message, cause);
    }

    public LoginFailedException(Throwable cause) {
        super(cause);
    }

    public LoginFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

异常捕获处理

component模块-mvc-config下配置一个 CrowdExceptionResolver.java 类用于定义异常捕获后的执行逻辑

package com.crowdfunding.mvc.config;

import com.crowdfunding.CrowdConstant;
import com.crowdfunding.CrowdUtils;
import com.crowdfunding.ResultEntity;
import com.crowdfunding.exception.AccessForbiddenException;
import com.crowdfunding.exception.LoginFailedException;
import com.google.gson.Gson;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * ControllerAdvice注解表示当前类是一个基于注解的异常处理类
 */
@ControllerAdvice
public class CrowdExceptionResolver {
    /**
     * 异常处理的私有通用方法
     * @param viewName  要跳转到的错误页面的名称
     * @param exception 要处理的异常(公共方法使用多态接收)
     * @param request   当前出现异常的请求
     * @param response  当前处理异常的响应
     * @return  ModelAndView
     */
    private ModelAndView commonResolve(
            String viewName, Exception exception,
            HttpServletRequest request, HttpServletResponse response) {

        // 判断当前请求类型
        final boolean judgeRequestType = CrowdUtils.judgeRequestType(request);
        if (judgeRequestType) {
            // 如果是Ajax请求,就创建一个ResultEntity对象
            final ResultEntity<Object> resultEntity = 							ResultEntity.failedWithMessage(exception.getMessage());

            // 创建一个gson对象 用来转换json
            final Gson gson = new Gson();
            final String json = gson.toJson(resultEntity);

            // 将json作为返回体返回给浏览器
            try {
                response.getWriter().write(json);
            } catch (IOException e) {
                e.printStackTrace();
            }

            // 由于上面已经通过原生的response对象返回了响应,所以不提供ModelAndView对象
            return null;
        }
        // 如果是Ajax请求,就创建ModelAndView对象
        final ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject(CrowdConstant.ATTR_NAME_EXCEPTION, exception);
        modelAndView.setViewName(viewName);

        // 返回ModelAndView对象
        return modelAndView;
    }

    /**
     * 处理用户名密码错误的异常
     * @param exception 实际捕获的异常类型
     * @param request   当前请求对象
     * @param response  响应体
     * @return  ModelAndView
     */
    @ExceptionHandler(value = LoginFailedException.class)
    public ModelAndView resolveLoginFailedException(
            LoginFailedException exception, HttpServletRequest request, HttpServletResponse response) {
        String viewName = "admin-login";
        // 调用通用方法进行处理
        return commonResolve(viewName, exception, request, response);
    }
}

handler中处理登录的方法

package com.crowdfunding.mvc.handler;

import com.crowdfunding.CrowdConstant;
import com.crowdfunding.entity.Admin;
import com.crowdfunding.service.api.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpSession;

@Controller
public class AdminHandler {
    @Autowired
    private AdminService adminService;

    /**
     * 用户登录的Controller
     * @param loginAcct 用户名
     * @param userPswd  密码
     * @param session   Session域对象
     * @return 登录成功重定向到管理员后台主页面
     */
    @RequestMapping("/admin/do/login")
    public String doLogin(@RequestParam("loginAcct") String loginAcct, @RequestParam("userPswd") String userPswd,
                          HttpSession session) {
        // 通过service判断用户是否存在,如果返回admin对象,证明用户名密码正确
        Admin admin = adminService.getAdminByLoginAcct(loginAcct, userPswd);

        //将登录成功返回的admin存入Session域中
        session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN, admin);
        // 此处使用重定向方法,将页面重定向到主页面,这个重定向在mvc的配置文件中通过view-controller标签直接指定映射关系
        return "redirect:/admin/to/main/page";
    }
}

Service实现的登录业务逻辑

package com.crowdfunding.service.impl;

import com.crowdfunding.CrowdConstant;
import com.crowdfunding.CrowdUtils;
import com.crowdfunding.entity.Admin;
import com.crowdfunding.entity.AdminExample;
import com.crowdfunding.exception.LoginFailedException;
import com.crowdfunding.mapper.AdminMapper;
import com.crowdfunding.service.api.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;


@Service
public class AdminServiceImpl implements AdminService {
    @Autowired
    private AdminMapper adminMapper;
    @Override
    public void saveAdmin(Admin admin) {
        adminMapper.insert(admin);
    }

    @Override
    public List<Admin> getAll() {
        /*通过Example查询 给一个空的就可以返回全部内容*/
        return adminMapper.selectByExample(new AdminExample());
    }

    @Override
    public Admin getAdminByLoginAcct(String loginAcct, String userPswd) {
        // 创建AdminExample对象
        final AdminExample adminExample = new AdminExample();
        // 创建AdminExample.Criteria 对象
        final AdminExample.Criteria criteria = adminExample.createCriteria();
        // 在criteria中封装查询条件
        criteria.andLoginAcctEqualTo(loginAcct);
        // 调用adminMapper方法执行查询
        final List<Admin> adminList = adminMapper.selectByExample(adminExample);
        
        if (adminList == null || adminList.size() == 0) {
            // 判断返回结果是否为空,如果为空则抛出异常
            throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
        }
        if (adminList.size() > 1) {
            // 如果数据结果不唯一,也抛出异常
            throw new RuntimeException(CrowdConstant.MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE);
        }
        // 获取admin对象并再次判断
        final Admin admin = adminList.get(0);
        if (admin == null) {
            throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
        }

        // 如果不为空则将数据库中的密码从admin对象中取出
        final String userPswdDB = admin.getUserPswd();
        // 将表单提交的明文密码进行MD5加密
        final String userPswdMd5 = CrowdUtils.md5(userPswd);

        // 对密码进行比较
        if (!Objects.equals(userPswdDB, userPswdMd5)){
            // 如果比较结果不一样则抛出异常
            throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
        }
        // 如果一致则返回admin对象
        return admin;
    }
}

拦截器的使用方法

自定义拦截器的类

这里拿判断是否登录作为拦截的条件为例

package com.crowdfunding.mvc.interceptor;


import com.crowdfunding.CrowdConstant;
import com.crowdfunding.entity.Admin;
import com.crowdfunding.exception.AccessForbiddenException;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

// 登录功能发生在所有的操作之前,所以无需实现顶级接口,
// 只需要继承一个现有的实现类配置preHandle前置判断即可。
public class LoginInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 通过request对象获取Session对象
        final HttpSession session = request.getSession();

        // 尝试从Session域中获取Admin对象
        final Admin admin = (Admin)session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN);
        // 判断admin对象是否为空
        if (admin == null) {
            // 抛出异常
            throw new AccessForbiddenException(CrowdConstant.MESSAGE_ACCESS_FORBIDDEN);
        }
        // 如果admin不为空,则正常放行
        return true;
    }
}

配置拦截器拦截规则

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">
   
    ......

    <!--
        配置view-controller 作用是直接把请求地址和视图名称关联起来,就不必写handler方法了
        换个说法,凡是get请求基本上都可以写到这里
    -->
    <mvc:view-controller path="/admin/to/login/page" view-name="admin-login"/>
    <mvc:view-controller path="/admin/to/main/page" view-name="admin-main"/>
    
    <!--注册拦截器-->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--配置需要拦截的资源 /* 对应一层路径 /**对应多层路径-->
            <mvc:mapping path="/**"/>
            <!--配置不需要拦截的路径-->
            <mvc:exclude-mapping path="/admin/to/login/page"/> <!--去登录页面的路径不拦截-->
            <mvc:exclude-mapping path="/admin/do/login"/> <!--登录本身的路径不拦截-->
            <mvc:exclude-mapping path="/admin/do/logout"/> <!--登出路径不拦截-->
            <!--配置拦截器的类-->
            <bean class="com.crowdfunding.mvc.interceptor.LoginInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

实现分页功能

添加依赖

分页功能的实现要使用一个插件PageHelper,首先需要确认父工程中已经引入了相关依赖

<!--mybatis分页-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.0.0</version>
</dependency>

还有一点需要注意,分页功能很多地方可以用到,比如筛选功能可能会用到基于关键字的结果的分页,查询所有可能用到所有结果的分页,跳转页码又需要用到获取结果后要去往哪一页。所以说需要写一个通过用的分页显示功能,就需要以下几个属性:

keyword:查询关键字,可以为空,为空代表我要查询所有数据

pageNum:查询完之后要去哪一页

pageSize:每一页显示多少条数据

注入插件

然后在spring整合mybatis的xml配置文件中,找到关于SqlSessionFactoryBean的配置,加入插件

<!--配置SqlSessionFactoryBean工厂整合Mybatis-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!--指定mybatis全局文件的位置-->
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <!--配置Mapper.xml配置文件的位置-->
    <property name="mapperLocations" value="classpath:mybatis/mapper/*Mapper.xml"/>
    <!--配置数据源-->
    <property name="dataSource" ref="dataSource"/>

    <!--配置插件-->
    <property name="plugins">
        <array>
            <!--配置PageHelper插件-->
            <bean class="com.github.pagehelper.PageHelper">
                <property name="properties">
                    <props>
                        <!--配置数据库方言,告诉PageHelper当前使用的数据库-->
                        <prop key="dialect">mysql</prop>
                        <!--
                            配置页码的合理化修正,在1~总页数之间修正代码,将不合理的代码修正到合理的范围内
                            比如要个-1页 就给修正到第一页,总共20页,请求第50页,就给修正到20页
                        -->
                        <prop key="reasonable">true</prop>
                    </props>
                </property>
            </bean>
        </array>
    </property>
</bean>

编写SQL语句并声明方法

语句的编写要在*Mapper.xml(这里以AdminMapper.xml为例)中进行,这个文件在一开始的时候提到了是通过mybatis的逆向构建得到的,逆向构建是通过xml文件配置SQL语句的,所以将其中的mapper.xml配置文件夹放在了webui模块中统一做了管理,而生成的实体类文件以及*Example文件都放在了实体类entity模块中,对应的接口文件放在了component模块中

<!--在AdminMapper.xml文件中加入下列SQL语句-->

<!--
    自定义的通过关键字查询login_acct中包含keyword的信息
    concat("%", #{keyword}, "%")用于拼接字符串,如果没传关键字,得到的就是""空字符串
-->
<select id="selectAdminByKeyword" resultMap="BaseResultMap">
    select id, login_acct, user_name, user_pswd, email, create_time
    from t_admin
    where
        login_acct like concat("%", #{keyword}, "%") or
        user_name like concat("%", #{keyword}, "%") or
        email like concat("%", #{keyword}, "%")
</select>

语句写好以后,需要在对应的AdminMapper接口中声明方法。

package com.crowdfunding.mapper;

import com.crowdfunding.entity.Admin;
import com.crowdfunding.entity.AdminExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;

public interface AdminMapper {
    int countByExample(AdminExample example);

    int deleteByExample(AdminExample example);

	...
	
    // 根据关键字查询进行模糊查询
    List<Admin> selectAdminByKeyword(String keyword);
}

实现查询并将结果分页

mapper层代码实现之后,我们可以通过selectAdminByKeyword方法从数据库中查询到需要的信息,然后就可以通过service层处理了。

再写具体的实现代码之前,首先需要在service的接口中实现方法

package com.crowdfunding.service.api;

import com.crowdfunding.entity.Admin;
import com.github.pagehelper.PageInfo;

import java.util.List;

public interface AdminService {
    
	...
        
    /**
     * 获取分页
     * @param keyword   查询关键字
     * @param pageNum   要去第几页
     * @param pageSize  每页显示条数
     * @return  PageInfo<Admin>
     */
    PageInfo<Admin> getPageInfo(String keyword, Integer pageNum, Integer pageSize);
}

然后在在service的实现类中实现查询并将结果分页功能

package com.crowdfunding.service.impl;

import com.crowdfunding.CrowdConstant;
import com.crowdfunding.CrowdUtils;
import com.crowdfunding.entity.Admin;
import com.crowdfunding.entity.AdminExample;
import com.crowdfunding.exception.LoginFailedException;
import com.crowdfunding.mapper.AdminMapper;
import com.crowdfunding.service.api.AdminService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;


@Service
public class AdminServiceImpl implements AdminService {
    @Autowired
    private AdminMapper adminMapper;
    
  	...
    
    /**
     * 根据关键字查询并且执行分页
     * @param keyword   查询关键字
     * @param pageNum   要去第几页
     * @param pageSize  每页显示条数
     * @return PageInfo<Admin>
     */
    @Override
    public PageInfo<Admin> getPageInfo(String keyword, Integer pageNum, Integer pageSize) {
        // 调用PageHelper的静态方法开启分页功能
        PageHelper.startPage(pageNum,pageSize);
        // 执行查询
        final List<Admin> adminList = adminMapper.selectAdminByKeyword(keyword);
        // 封装到PageInfo对象中
        return new PageInfo<>(adminList);
    }
}

编写handler控制器

控制器中需要有几个点注意一下,keyword在定义SQL语句时,是可以为有值或者""这两种情况的,所以说,需要给keyword关键字设置默认值为"";不是所有的操作都会传递过来pageNum参数,所以也需要一个默认值,默认跳转到第一页;同样pagSize也是一样的,默认值设置为5条数据/页

package com.crowdfunding.mvc.handler;

import com.crowdfunding.CrowdConstant;
import com.crowdfunding.entity.Admin;
import com.crowdfunding.service.api.AdminService;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpSession;

@Controller
public class AdminHandler {
    @Autowired
    private AdminService adminService;

    /**
     * 获取用户列表页面信息
     * @param keyword   关键字
     * @param pageNum   跳转到哪一页
     * @param pageSize  每页显示多少条数据
     * @param modelMap  储存数据的Map集合
     * @return  去往admin-page页面
     */
    @RequestMapping("/admin/get/page")
    public String getPageInfo(
        @RequestParam(value = "keyword", defaultValue = "") String keyword, // 默认值设置为空
        @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, // 默认去第一页
        @RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize, // 默认每一页显示5条信息
        ModelMap modelMap
    ) {
        // 调用service方法获取PageInfo对象
        final PageInfo<Admin> pageInfo = adminService.getPageInfo(keyword, pageNum, pageSize);
        // 将PageInfo对象放到ModelMap里
        // 常量CrowdConstant.ATTR_NAME_PAGE_INFO = "pageInfo"
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_PAGE_INFO, pageInfo); 
        return "admin-page";
    }
}

分页功能的前端应用实现

前端的应用时间通过Pagination插件实现,首先在需要使用分页功能的页面引入这两个文件

<link rel="stylesheet" href="css/pagination.css"/>
<script type="text/javascript" src="jquery/jquery.pagination.js"></script>

然后在需要设置分页的地方配置元素

<table class="table  table-bordered">
    <thead>
    <tr>
        <th width="30"><input type="checkbox"></th>
        <th width="30">#</th>
        <th>账号</th>
        <th>名称</th>
        <th>邮箱地址</th>
        <th width="100">操作</th>
    </tr>
    </thead>
    <tbody>
    <c:if test="${ empty requestScope.pageInfo.list }">
        <tr>
            <td colspan="6" align="center">抱歉!没有查询到您要的数据!</td>
        </tr>
    </c:if>
    <c:if test="${ !empty requestScope.pageInfo.list }">
        <c:forEach items="${ requestScope.pageInfo.list }" var="admin" varStatus="myStatus">
            <tr>
                <td><input type="checkbox"></td>
                <td>${ myStatus.count }</td>
                <td>${ admin.loginAcct }</td>
                <td>${ admin.userName }</td>
                <td>${ admin.email }</td>
                <td>
                    <a href="" class="btn btn-success btn-xs"><i
                            class="glyphicon glyphicon-check"></i></a>
                    <a href="" class="btn btn-primary btn-xs"><i
                            class=" glyphicon glyphicon-pencil"></i></a>
                    <a href="" class="btn btn-danger btn-xs"><i
                            class=" glyphicon glyphicon-remove"></i></a>
                </td>
            </tr>
        </c:forEach>
    </c:if>
    </tbody>
    <tfoot>
    <tr>
        <td colspan="6" align="center">
            <!-- 此处id必须设置为Pagination -->
            
            <div id="Pagination" class="pagination"><!-- 这里显示分页 --></div>
        </td>
    </tr>
    </tfoot>
</table>

在配置好分页器的容器元素后,需要写一段js配置代码

<link rel="stylesheet" href="css/pagination.css"/>
<script type="text/javascript" src="jquery/jquery.pagination.js"></script>
<!-- 此处配置Pagination分页器 -->
<script type="text/javascript">
    $(function () {
        // 调用函数初始化分页导航条
        initPagination();
    })
    function initPagination() {
        // 获取pageInfo中的总记录数
        var totalRecord = ${requestScope.pageInfo.total};
        // 声明一个对象用于导航条的相关配置
        var properties = {
            num_edge_entries: 3,                                // 边缘页数
            num_display_entries: 4,                             // 主体页数
            callback: pageSelectCallback,                       // 页码点击跳转,一个回调函数
            items_per_page: ${requestScope.pageInfo.pageSize},  // 每页现实的数据条数
            current_page: ${requestScope.pageInfo.pageNum - 1}, //当前页码。 Pagination 内部管理页码的index是从0开始的,所以传过来的值要减一
            prev_text: "上一页",
            next_text: "下一页"
        }
        // 生成页码导航条
        $("#Pagination").pagination(totalRecord, properties);
    }
    // 回调函数的含义:声明一个函数给程序调用而非给自己调用
    // 用户点击“上一页、下一页、以及具体页码时候,用这个回调函数执行跳转”
    //  pageIndex是Pagination传给我们的,index从0开始
    function pageSelectCallback(pageIndex, jQuery) {
        var pageNum = pageIndex + 1;
        // 跳转页面,跳转页面需要考虑到一个问题,就是如果是关键词查询后的结果需要跳转页面,每次请求都需要加上关键字
        // 实际上通过关键字筛选到返回结果页面,是一次请求,所以说直接通过参数取出keyword就可以
        window.location.href = "admin/get/page?pageNum=" + pageNum + "&keyword=${param.keyword}";
        // 由于每一个页码按钮都是一个超链接,所以在这个函数最后,要取消超链接的默认跳转行为
        return false;
    }
</script>

*注意事项:这个分页器的jquery代码在实际使用中有个小Bug,需要修改一开始引用时候的jquery.pagination.js文件中的代码

return this.each(function() {
   /**
    * 计算最大分页显示数目
    */
   function numPages() {
      return Math.ceil(maxentries/opts.items_per_page);
   }  

   ...

   // 所有初始化完成,绘制链接
   drawLinks();
   // 回调函数
   // 此处代码需要注释掉,这两句代码的含义是初始化完成后前端页面绘制分页器,然后执行回调函数刷新,
   // 但是刷新之后又会重新舒适化,初始化之后又会调用回调函数,边形成了一个死循环。
   // 而我们要达到的目的是在用户点击的时候进行刷新即可,所以这个回调函数需要注释掉,使用我们在页面配置的js代码中的自定义的回调
   //opts.callback(current_page, this);
});

Ajax动态页面加载并实现分页

角色数据获取

controller控制器代码

@Controller
public class RoleHandler {

    @Autowired
    private RoleService roleService;

    @ResponseBody // 因为是采用的Ajax请求方式,所以必须声明@ResponseBody注解才能发送json数据给前端
    @RequestMapping("/role/get/page/info") // 定义获取数据的访问路径
    public ResultEntity<PageInfo<Role>> getPageInfo(
        	// 因为角色数据同样涉及到分页功能,所以此处依旧声明三个和分页有关的参数
            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
            @RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize,
            @RequestParam(value = "keyword", defaultValue = "") String keyword
            ) {
        // 调用service方法,获取分页数据
        final PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);
        // 返回ResultEntity获取结果,如果结果异常,直接走异常映射
        return ResultEntity.successWithData(pageInfo);
    }
}

service层代码

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public PageInfo<Role> getPageInfo(Integer pageNum, Integer pageSize, String keyword) {
        // 开启分页功能
        PageHelper.startPage(pageNum, pageSize);
        // 执行查询
        final List<Role> roleList = roleMapper.selectRoleByKeyword(keyword);
        // 封装为pageInfo对象返回
        return new PageInfo<>(roleList);
    }
}

dao层代码是基于mybatis框架逆向构建生成的,没有自定义部分,这里不多做赘述

定义view-controller路由

Ajax实现动态渲染主要工作就在前端,通过js实现与后端数据的交互,首先,需要在springmvc的配置文件中,定义view-controller

<mvc:view-controller path="/role/to/page" view-name="role-page"/> <!--到角色信息界面的路由-->

创建role-page.jsp页面

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="UTF-8">
<%--引入共同head模版--%>
<%@include file="include-head.jsp" %>
<link rel="stylesheet" href="css/pagination.css"/>
<script type="text/javascript" src="jquery/jquery.pagination.js"></script>
<!-- 关于js代码到底是从本页面声明还是引用外部文件的问题,
如果是封装的function函数来调用,就在外部声明js文件,然后引用进来
如果是操作本页面的元素,比如绑定事件之类的操作就在本页面直接写-->
<script src="crowd/my-role.js"></script><!-- 引入外部js文件 -->
<body>
<%--引入共同的nav导航栏--%>
<%@include file="include-nav.jsp" %>
<div class="container-fluid">
    <div class="row">
        <%--引入共同的sidebar侧边菜单栏--%>
        <%@include file="include-sidebar.jsp" %>
        <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
            <%--在此编写页面主体部分--%>
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><i class="glyphicon glyphicon-th"></i> 数据列表</h3>
                </div>
                <div class="panel-body">
                    <form class="form-inline" role="form" style="float:left;">
                        <div class="form-group has-feedback">
                            <div class="input-group">
                                <div class="input-group-addon">查询条件</div>
                                <%--form中如果只有一个input,那么enter键会自动执行提交,所以加一个隐藏input解决这个问题--%>
                                <input type="text" id="hiddenText" name="hiddenText" style="display:none" />
                                <input id="keywordInput" class="form-control has-success" type="text"
                                       placeholder="请输入查询条件">
                            </div>
                        </div>
                        <button id="searchBtn" type="button" class="btn btn-warning"><i
                                class="glyphicon glyphicon-search"></i> 查询
                        </button>
                    </form>
                    <button id="batchRemoveBtn" type="button" class="btn btn-danger"
                            style="float:right;margin-left:10px;"><i
                            class=" glyphicon glyphicon-remove"></i> 删除
                    </button>
                    <button id="showModelBtn" type="button" class="btn btn-primary" style="float:right;"><i
                            class="glyphicon glyphicon-plus"></i> 新增
                    </button>
                    <br>
                    <hr style="clear:both;">
                    <div class="table-responsive">
                        <table class="table table-bordered">
                            <thead>
                            <tr>
                                <th width="30"><input id="summaryBox" type="checkbox"></th>
                                <th width="30">#</th>
                                <th>名称</th>
                                <th width="100">操作</th>
                            </tr>
                            </thead>
                            <tbody id="rolePageBody"><!-- 这里通过js动态注入ajax交互获取的数据 --></tbody>
                            <tfoot>
                            <tr>
                                <td colspan="6" align="center">
                                    <div id="Pagination" class="pagination"><!-- 这里显示分页 --></div>
                                </td>
                            </tr>
                            </tfoot>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

路由加页面已经可以实现页面的访问,但是现在访问是看不到角色的数据的,所以下一步就是通过ajax获取角色数据,用户通过路由到角色数据页面,第一次请求应该是初始化的时候就完成的,这样才可以实现数据的自动装载,所以首先在页面中定义js代码实现初始化

初始化前端页面

<script type="text/javascript">
    $(function () {
        // 为分页操作准备初始化数据,定义在全局变量中有利于后期随时调用
        window.pageNum = 1;
        window.pageSize = 5;
        window.keyword = "";
        // 调用执行分页的函数,显示分页效果,generatePage()函数定义在上面引用的外部文件crowd/my-role.js中
        generatePage();
    })
</script>

crowd/my-role.js文件

// 执行分页并生成页面效果,任何时候调用这个函数都会重新加载页面
function generatePage() {
    // 获取分页数据PageInfo
    getPageInfoRemote();
}

// 远程访问服务器端程序,获取pageInfo数据
function getPageInfoRemote() {
    $.ajax({
        "url": "role/get/page/info/",
        "type": "post",
        "data": {
            // 之所以要将分页的三个属性定义为全局变量,就是方面在任何需要的地方直接执行修改或者数据交互的调用
            "pageNum": window.pageNum,
            "pageSize": window.pageSize,
            "keyword": window.keyword
        },
        
        "dataType": "json", 
        "success": function (response) {
            if (response.result === "SUCCESS") {
                console.log(response.data);
                // 填充表格
                fillTableBody(response.data);
            }
            if (response.result === "FAILED") {
                layer.msg(response.message);
            }
        },
        "error": function (response) {
            layer.msg("请求异常,状态码:" + response.status);
        }
    });
}

// 填充表格
function fillTableBody(pageInfo) {
    //每一次调用函数,都会请求新的数据然后执行填充,所以此处需要先清空table body中的旧数据
    var rolePageBody = $("#rolePageBody");
    rolePageBody.empty();
    // 同时将导航条动态内容也清空
    $("#Pagination").empty();

    // 判断pageInfo是否有效
    if (pageInfo == null || pageInfo.list == null || pageInfo.list.length === 0) {
        rolePageBody.append("<tr class='text-center'><td colspan='4' >抱歉,没有查询到你搜索的数据</td></tr>>");
        return false;
    }
    for (let i = 0; i < pageInfo.list.length; i++) {
        var role = pageInfo.list[i]; // 取出返回的数据列表
        var roleId = role.id;
        var roleName = role.name;
        // 开始定义填充内容
        var checkBoxTd = "<td><input id='"+roleId+"' class='itemBox' type='checkbox'></td>"; // 多选框
        var numberTd = "<td>" + (i + 1) + "</td>"; // 数据序号
        var roleNameTd = "<td>" + roleName + "</td>"; // 角色名称

        var checkBtn = "<button type='button' class='btn btn-success btn-xs' style='margin-right: 4px'><i class='glyphicon glyphicon-check'></i></button>";
        // 通过id属性,将roleId携带过去,传递到后续的单击响应函数中,在单机响应函数中使用this.id 就可以拿到
        // 这里是为了后面的增删改所准备
        var pencilBtn = "<button type='button' id='"+roleId+"' class='btn btn-primary btn-xs pencilBtn' style='margin-right: 4px'><i class='glyphicon glyphicon-pencil'></i></button>";
        var removeBtn = "<button type='button' id='"+roleId+"' class='btn btn-danger btn-xs removeBtn'><i class='glyphicon glyphicon-remove'></i></button>";
        var buttonTd = "<td>" + checkBtn + pencilBtn + removeBtn + "</td>"

        // 填充table body
        rolePageBody.append("<tr>" + checkBoxTd + numberTd + roleNameTd + buttonTd + "</tr>")

        // 生成分页导航条
        generateNavigator(pageInfo);
    }
}

// 生成分页页码导航条
function generateNavigator(pageInfo) {
    // 获取总记录数
    var totalRecord = pageInfo.total;

    // 声明分页参数
    var properties = {
        num_edge_entries: 3,                // 边缘页数
        num_display_entries: 4,             // 主体页数
        callback: pageSelectCallback,       // 页码点击跳转,一个回调函数
        items_per_page: pageInfo.pageSize,  // 每页现实的数据条数
        current_page: pageInfo.pageNum - 1, //当前页码。 Pagination 内部管理页码的index是从0开始的,所以传过来的值要减一
        prev_text: "上一页",
        next_text: "下一页"
    }

    // 生成页码导航条
    $("#Pagination").pagination(totalRecord, properties);
}

// 翻页时的回调函数
function pageSelectCallback(pageIndex, jQuery) {
    // 点击跳转页面,Pagination 内部管理页码的index是从0开始的,所以点解所做的跳转需要 + 1
    // pageIndex指的是用户点击跳转的索引,比如点第五页,实际上我们需要+1才能真正到大第五页
    window.pageNum = pageIndex + 1;
    // 调用分页函数,回调函数中再次调用分页函数,实际上并不会出现嵌套死循环,因为每一次回调函数的执行,都必须是用户做了跳转操作。
    generatePage();
    // 取消页面跳转超链接默认行为
    return false;
}

实现关键字搜索

关键字的搜索功能,我们直接定义在页面中的js代码块中即可

<script type="text/javascript">
    $(function () {
        // 为分页操作准备初始化数据,定义在全局变量中有利于后期随时调用
        window.pageNum = 1;
        window.pageSize = 5;
        window.keyword = "";
        // 调用执行分页的函数,显示分页效果,generatePage()函数定义在上面引用的外部文件crowd/my-role.js中
        generatePage();
        
        // 关键字查询的处理办法
        // 点击查询按钮时,获取输入框中的文字,并赋值给window.keyword变量
        $("#searchBtn").click(function () {
            window.keyword = $("#keywordInput").val();
            // 搜索后的提交会携带原有的window.pageNum,所以重置到第一页,保证搜索结果可以看到第一条信息
            window.pageNum = 1;
            // 执行分页函数,刷新页面
            generatePage();
            return false;
        });
    })
</script>

至此,角色数据获取功能基本已经实现,代码的重点有三处:

第一:将分页所需要的参数定义为全局变量,这样方便在任何地方修改和引用

第二:通过ajax请求获取后端数据,也就是getPageInfoRemote()函数

第三:表格的填充实现主要就是基于DOM操作,这个有一个小知识点就是通过属性携带我们其他地方需要引用的值来完成其他操作。

新增角色数据

因为是通过ajax实现数据的动态交互,所以说增删改的操作我们全部懂采用模态框的形式,避免页面的跳转

controller控制器代码

@Controller
public class RoleHandler {

    @Autowired
    private RoleService roleService;
    ...
        
	// 新增角色
    @ResponseBody
    @RequestMapping("/role/save")
    public ResultEntity<String> saveRole(Role role) {
        roleService.saveRole(role);
        return ResultEntity.successWithoutData();
    }
}

service层代码

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public void saveRole(Role role) {
        roleMapper.insert(role);
    }
}

编写新增modal的jsp文件

模态框的使用,依然写到单独的文件然后通过外部文件引用使用

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<!DOCTYPE html>
<div id="roleAddModal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">新建角色</h5>
            </div>
            <div class="modal-body">
                <form class="form-signin" role="form">
                    <div class="form-group has-success has-feedback">
                        <%--form中如果只有一个input,那么enter键会自动执行提交,所以加一个隐藏input解决这个问题--%>
                        <input type="text" id="hiddenText" name="hiddenText" style="display:none" />
                        <input type="text" name="roleName" class="form-control" placeholder="请输入角色名称" autofocus>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="$('#roleAddModal input[name=roleName]').val('')">取消</button>
                <button type="button" class="btn btn-primary" id="saveRoleBtn">保存</button>
            </div>
        </div>
    </div>
</div>

然后在页面添加引用,关于modal代码的引用问题,建议放在代码的最后面统一引用,代码更为简洁

<body>
    ...
    <%@include file="modal-role-add.jsp"%>
</body>

编写js实现新增角色

<script type="text/javascript">

    $(function () {
        
        ...
        
        // RoleAddModal
        $("#showModelBtn").click(function () {
            $("#roleAddModal").modal('show');
        });

        // 给新增模态框中的保存按钮保存单机响应函数
        $("#saveRoleBtn").click(function () {
            var addRoleInput = $("#roleAddModal input[name=roleName]")
            // 获取输入框数据并去除前后空格
            var roleName = $.trim(addRoleInput.val());
            // 发送ajax请求
            $.ajax({
                "url": "role/save/",
                "type": "post",
                "data": {
                    "name": roleName
                },
                "dataType": "json",
                "success": function (response) {
                    if (response.result === "SUCCESS") {
                        layer.msg("操作成功");
                        // 如果操作成功,就把新添加的角色的名称设定为查询关键字,便于在最后刷新页面
                        window.keyword = $.trim(addRoleInput.val());
                        // 清理模态框
                        addRoleInput.val("")
                        // 重新加载分页数据
                        generatePage()
                    }
                    if (response.result === "FAILED") {
                        layer.msg("操作失败:" + response.message);
                        // 清理模态框
                        addRoleInput.val("")
                    }
                },
                "error": function (response) {
                    layer.msg(response.status + " " + response.statusText);
                }
            });
            // 关闭模态框
            $("#roleAddModal").modal("hide");
        });
    })
</script>

修改角色数据

修改角色数据要稍微复杂一些,因为整个数据都是动态生成的,所以如何实现元素选择以及获取role的id需要费一些力气,这也是为什么之前提到需要通过属性将role的id值传输过来的原因,其次,因为是动态数据,所以往常的元素选择方式是无法实现的,需要用到jQuery的on()函数

controller控制器代码

@Controller
public class RoleHandler {

    @Autowired
    private RoleService roleService;
    
    @ResponseBody
    @RequestMapping("/role/update")
    public ResultEntity<String> updateRole(Role role) {
        roleService.updateRole(role);
        return ResultEntity.successWithoutData();
    }
}

service层代码

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public void updateRole(Role role) {
        roleMapper.updateByPrimaryKey(role);
    }
}

编写修改modal的jsp文件

这里同样通过modal文件引入形式进行

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<!DOCTYPE html>
<div id="editAddModal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">新建角色</h5>
            </div>
            <div class="modal-body">
                <form class="form-signin" role="form">
                    <div class="form-group has-success has-feedback">
                        <%--form中如果只有一个input,那么enter键会自动执行提交,所以加一个隐藏input解决这个问题--%>
                        <input type="text" id="hiddenText" name="hiddenText" style="display:none" />
                        <input type="text" name="roleName" class="form-control" placeholder="请输入角色名称" autofocus>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" id="updateRoleBtn">更新</button>
            </div>
        </div>
    </div>
</div>
<body>
    ...
    <%@include file="modal-role-add.jsp"%>
    <%@include file="modal-role-edit.jsp"%>
</body>

编写js实现角色更新

<script type="text/javascript">

 $(function () {
     /*
        * 给修改按钮绑定单击响应函数,因为按钮是动态生成的,所以普通的绑定方式是无效的
        * 找到所有动态生成的元素附着的静态元素
        * 然后通过jQuery的on函数给附着的动态元素绑定单击响应事件
        * on函数的第一个属性指定要绑定的事件类型
        * on函数的第二个属性指定给谁绑定
        * on函数的第三个属性指定要做什么动作,即回调函数
        * */
        $("#rolePageBody").on("click", ".pencilBtn", function () {
            // 设置模态框的信息回显
            $("#editAddModal input[name=roleName]").val($(this).parent().prev().text());
            $("#editAddModal").modal("show");
            // 这里需要直接将要修改的数据的id也就是之前赋值给button的id值取出来并放到window的全局变量中
            window.roleId = this.id;
        });
        // 给modal中的更新按钮设置单击相应函数,执行ajax请求更新数据
        $("#updateRoleBtn").click(function () {
            $.ajax({
                "url": "role/update",
                "type": "post",
                "data": {
                    "id": window.roleId,
                    "name": $.trim($("#editAddModal input[name=roleName]").val()),
                },
                "dataType": "json",
                "success": function (response) {
                    if (response.result === "SUCCESS") {
                        layer.msg("更新成功");
                        // 如果操作成功,就把新修改的角色的名称设定为查询关键字,便于在最后刷新页面
                        window.keyword = $.trim($("#editAddModal input[name=roleName]").val());
                        generatePage()
                    }
                    if (response.result === "FAILED") {
                        layer.msg("更新失败:" + response.message);
                    }
                },
                "error": function (response) {
                    layer.msg(response.status + response.statusText);
                }
            });
            // 不管成功还是失败,都将模态框关闭
            $("#editAddModal").modal("hide")
        });
	})
</script>

角色数据删除

数据删除分为两种形式,一种是单挑数据删除,一种是批量删除,所以在删除上,我们可以将前后端的代码都同一一下选择批量删除的方法,也就是说传回后端的数据是列表的形式[5, 8, 12]。而前端则同一ajax数据交互部分,不管是单条数据还是批量,拿到id值然后加入到列表中,再通过ajax传入后端即可。

后端代码实现

controller控制器

@Controller
public class RoleHandler {
    
    @Autowired
    private RoleService roleService;

    @ResponseBody
    @RequestMapping("/role/remove")
    // 因为前端将会传递json字符串参数过来,所以参数这里需要使用@RequestBody注解实现接收
    public ResultEntity<String> removeRoleByIdList(@RequestBody List<Integer> roleIdList) {
        roleService.removeRole(roleIdList);
        return ResultEntity.successWithoutData();
    }
}

service层代码

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public void removeRole(List<Integer> roleIdList) {
        final RoleExample roleExample = new RoleExample();
        final RoleExample.Criteria criteria = roleExample.createCriteria();
        // delete from t_role where id in (5, 8, 12)
        criteria.andIdIn(roleIdList);
        roleMapper.deleteByExample(roleExample);
    }
}

前端代码实现

modal的编写及引入

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<!DOCTYPE html>
<div id="deleteRoleConfirmModal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">新建角色</h5>
            </div>
            <div class="modal-body">
                <h5>是否删除一下信息?</h5>
                <div id="roleNameDiv" class="text-center"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="">取消</button>
                <button type="button" class="btn btn-primary" id="deleteRoleBtn">删除</button>
            </div>
        </div>
    </div>
</div>
<body>
    ...
    <%@include file="modal-role-add.jsp"%>
    <%@include file="modal-role-edit.jsp"%>
    <%@include file="modal-role-confirm.jsp"%>
</body>

showConfirmModal(roleArray)函数用来处理删除动作,也是前端针对于单条或者批量的一个共同的删除逻辑,参数是一个role的数组,主要用来显示删除提示信息以及向后端传递要删除的数据的id列表,此函数同样放在外部引用文件crowd/my-role.js中

...

// 用于处理删除的函数
function showConfirmModal(roleArray) {
    //打开模态框
    $("#deleteRoleConfirmModal").modal("show");
    var roleNameDiv = $("#roleNameDiv");
    // 清空roleNameDiv中的内容
    roleNameDiv.empty();
    // 定义全局变量数组,储存需要删除的role的id
    window.roleIdArray = [];

    
    for (var i = 0; i < roleArray.length; i++) {
        // 遍历数组获取roleName,用来在删除信息用的modal中填充需要删除的数据的名称
        var roleName = roleArray[i].name;
        roleNameDiv.append(roleName + "<br>");
        // 取出roleID添加到id数组中,后面用于ajax传递到后端执行删除
        var roleId = roleArray[i].id;
        window.roleIdArray.push(roleId);
    }
}

用于删除的函数编写完成后,我们需要先通过删除按钮调出modal框确认是否删除信息,再给modal中的确认删除的按钮绑定单击事件执行删除

<script type="text/javascript">

    $(function () {
 		...
            
        // 给单条删除数据的按钮绑定单击响应函数
        $("#rolePageBody").on("click", ".removeBtn", function () {
            // 获取要删除的信息的数组信息
            var roleArray = [{
                "name": $(this).parent().prev().text(),
                "id": this.id
            }];
            showConfirmModal(roleArray);
        });
        
        // modal中删除按钮的单击响应函数
        $("#deleteRoleBtn").click(function () {
            $.ajax({
                "url": "role/remove",
                "type": "post",
                "data": JSON.stringify(window.roleIdArray), // 向后端传递json数据必须转化为json字符串才能传输
                "contentType": "application/json;charset=UTF-8",
                "dataType": "json",
                "success": function (response) {
                    if (response.result === "SUCCESS") {
                        layer.msg("删除成功");
                        generatePage();
                    }
                    if (response.result === "FAILED") {
                        layer.msg("删除失败:" + response.message);
                    }
                },
                "error": function (response) {
                    layer.msg(response.status + response.statusText);
                }
            });
            $("#deleteRoleConfirmModal").modal("hide");
        });
</script>

批量删除操作需要解决两个问题,一个是多选框的状态绑定,全选/全不选的操作实现,再者就是执行删除操作的js代码

<script type="text/javascript">

    $(function () {
		...
            
        // 设置单选框和全选框的绑定关系
        // 设置点击全选/全不选设置其他多选框的状态
        $("#summaryBox").click(function () {
            var currentStatus = this.checked;
            $(".itemBox").prop("checked", currentStatus);
        });

        // 设置itemBox是否全选时,全选/全不选按钮的状态
        $("#rolePageBody").on("click", ".itemBox", function () {
            // 获取已经选中的itemBox数量
            var checkedNum = $(".itemBox:checked").length;
            // 获取全部复选框的数量
            var countBox = $(".itemBox").length;
            $("#summaryBox").prop("checked", checkedNum === countBox);
        });

        // 给批量删除的按钮绑定单击响应函数
        $("#batchRemoveBtn").click(function () {
            // 创建role数组
            var roleArray = [];
            // 遍历当前选中状态的多选框
            $(".itemBox:checked").each(function () {
                // 使用this引用当前遍历到的多选框
                var roleId = this.id;
                // 获取roleName
                var roleName = $(this).parent().next().next().text();

                roleArray.push({
                    "name": roleName,
                    "id": roleId
                });
            });
            // 判断roleArray的长度是否为0
            if (roleArray.length === 0) {
                layer.msg("请勾选需要删除的数据");
                return;
            }
            showConfirmModal(roleArray);
        });
    })
</script>

权限控制

权限控制主要通过spring Security实现,但前提是我们需要配置好分配角色的功能

第一层级的权限控制要通过给账号分配角色,来让账号拥有已分配的角色的权限,而账号和角色之间的关联关系,我们记录到数据库中的一张中间表inner_admin_role中,而这张表本身并不需要实体类,所以不需要通过mybatis逆向构建

创建中间表

CREATE TABLE inner_admin_role ( id INT PRIMARY KEY NOT NULL auto_increment, admin_id INT, role_id INT );

Controller控制器编写

@Controller
public class AssignHandler {
    @Autowired
    private AdminService adminService;
    @Autowired
    private RoleService roleService;

    @RequestMapping("/assign/to/assign/page")
    public String toAssignRolePage(@RequestParam("adminId") Integer adminId, ModelMap modelMap) {
        // 查询已分配的角色
        List<Role> assignedRoleList = roleService.getAssignedRole(adminId);
        // 查询未分配的角色
        List<Role> unAssignedRoleList = roleService.getUnAssignedRole(adminId);
        // 存入模型
        modelMap.addAttribute("assignedRoleList", assignedRoleList);
        modelMap.addAttribute("unAssignedRoleList", unAssignedRoleList);
        return "assign-role";
    }
}

创建service方法

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

	...    
    
    @Override
    public List<Role> getAssignedRole(Integer adminId) {
        return roleMapper.selectAssignedRole(adminId);
    }

    @Override
    public List<Role> getUnAssignedRole(Integer adminId) {
        return roleMapper.selectUnAssignedRole(adminId);
    }
}

编写sql语句

roleMapper接口编写selectAssignedRole、selectUnAssignedRole方法,并在mapper中实现sql语句配置

public interface RoleMapper {
    ...
    List<Role> selectAssignedRole(Integer adminId);

    List<Role> selectUnAssignedRole(Integer adminId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.crowdfunding.mapper.RoleMapper">
    ...
    <select id="selectAssignedRole" resultMap="BaseResultMap">
        select id, name from t_role where id in (select role_id from inner_admin_role where admin_id = #{adminId})
    </select>
    <select id="selectUnAssignedRole" resultMap="BaseResultMap">
        select id, name from t_role where id not in (select role_id from inner_admin_role where admin_id = #{adminId})
    </select>
    ...
</mapper>
java学习笔记
框架知识
  • 作者:JackLiu
  • 发表时间:2020-12-09 03:19:39
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 公众号转载:请在文末添加作者公共号二维码

评论列表