基于springboot搭建的web系统架构的方法步骤

从接触springboot开始,便深深的被它的简洁性深深的折服了,精简的配置,方便的集成,使我再也不想用传统的ssm框架来搭建项目,一大堆的配置文件,维护起来很不方便,集成的时候也要费力不少。从第一次使用springboot开始,一个简单的main方法,甚至一个配置文件也不需要(当然我是指的没有任何数据交互,没有任何组件集成的情况),就可以把一个web项目启动起来,下面总结一下自从使用springboot依赖,慢慢完善的自己的一个web系统的架构,肯定不是最好的,但平时自己用着很舒服。

1. 配置信息放到数据库里边

个人比较不喜欢配置文件,因此有一个原则,配置文件能不用就不用,配置信息能少些就少些,配置内容能用代码写坚决不用xml,因此我第一个想到的就是,能不能把springboot的配置信息写到数据库里,在springboot启动的时候自动去加载,而在application.properties里边只写一个数据源。最终找到了方法:

注意图中箭头指向的两行,构造了一个properties对象,然后将这个对象放到了springboot的启动对象application中,properties是一个类似map的key-value容器,springboot可以将其中的东西当做成原来application.properties中的内容一样,因此在properties对象的内容也就相当于写在了application.properties文件中。知道了这个之后就简单了,我们将原本需要写在application.properties中的所有配置信息写在数据库中,在springboot启动的时候从数据库中读取出来放到properties对象中,然后再将这个对象set到application中即可。上图中PropertyConfig.loadProperties()方法就是进行了这样的操作,代码如下:

PropertyConfig.java

public class PropertyConfig {

  /**
   * 生成Properties对象
   */
  public static Properties loadProperties() {
    Properties properties = new Properties();
    loadPropertiesFromDb(properties);
    return properties;
  }

  /**
   * 从数据库中加载配置信息
   */
  private static void loadPropertiesFromDb(Properties properties) {
    InputStream in = PropertyConfig.class.getClassLoader().getResourceAsStream("application.properties");
    try {
      properties.load(in);
    } catch (Exception e) {
      e.printStackTrace();
    }
    String profile = properties.getProperty("profile");
    String driverClassName = properties.getProperty("spring.datasource.driver-class-name");
    String url = properties.getProperty("spring.datasource.url");
    String userName = properties.getProperty("spring.datasource.username");
    String password = properties.getProperty("spring.datasource.password");

    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
      Class.forName(driverClassName);
      String tableName = "t_config_dev";
      if ("pro".equals(profile)) {
        tableName = "t_config_pro";
      }
      String sql = "select * from " + tableName;
      conn = DriverManager.getConnection(url, userName, password);
      pstmt = conn.prepareStatement(sql);
      rs = pstmt.executeQuery();
      while (rs.next()) {
        String key = rs.getString("key");
        String value = rs.getString("value");
        properties.put(key, value);
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        if (conn != null) {
          conn.close();
        }
        if (pstmt != null) {
          pstmt.close();
        }
        if (rs != null) {
          rs.close();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

}

代码中,首先使用古老的jdbc技术,读取数据库t_config表,将表中的key-value加载到properties中,代码中profile是为了区分开发环境和生产环境,以便于确定从那张表中加载配置文件,数据库中的配置信息如下:

这样以后,application.properties中就不用再写很多的配置信息,而且,如果将这些配置信息放到数据库中之后,如果起多个应用可是公用这一张表,这样也可以做到配置信息的公用的效果,这样修改以后,配置文件中就只有数据源的信息了:

profile代表使用哪个环境,代码中可以根据这个信息来从开发表中加载配置信息还是从生产表中加载配置信息。

2. 统一返回结果

一般web项目中,大多数都是接口,以返回json数据为主,因此统一一个返回格式很必要。在本示例中,建了一个BaseController,所有的Controller都需要继承这个类,在这个BaseController中定义了成功的返回和失败的返回,在其他业务的Controller中,返回的时候,只需要return super.success(xxx)或者return super.fail(xxx, xxx)即可,例:

说到这里,返回给前台的状态码,建议也是封装成一个枚举类型,不建议直接返回200、400之类的,不方便维护也不方便查询。那么BaseController里做了什么呢?如下:

定义一个ResultInfo类,该类只有两个属性,一个是Integer类型的状态码,一个是泛型,用于成功时返回给前台的数据,和失败时返回给前台的提示信息。

3. 统一异常捕获

在上一步中的Controller代码中看到抛出了一个自定义的异常,在Controller中,属于最外层的代码了,这个时候如果有异常就不能直接抛出去了,这里再抛出去就没有人处理了,服务器只能返回给前台一个错误,用户体验不好。因此,建议所有的Controller代码都用try-catch包裹,捕获到异常后统一进行处理,然后再给前台一个合理的提示信息。在上一步中抛出了一个自定义异常:

throw new MyException(ResultEnum.DELETE_ERROR.getCode(), "删除员工出错,请联系网站管理人员。", e);

该自定义异常有三个属性,分别是异常状态码,异常提示信息,以及捕获到的异常对象,接下来定义一个全局的异常捕获,统一对异常进行处理:

@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandle {

  /**
   * 处理捕获的异常
   */
  @ExceptionHandler(value = Exception.class)
  public Object handleException(Exception e, HttpServletRequest request, HttpServletResponse resp) throws IOException {
    log.error(AppConst.ERROR_LOG_PREFIX + "请求地址:" + request.getRequestURL().toString());
    log.error(AppConst.ERROR_LOG_PREFIX + "请求方法:" + request.getMethod());
    log.error(AppConst.ERROR_LOG_PREFIX + "请求者IP:" + request.getRemoteAddr());
    log.error(AppConst.ERROR_LOG_PREFIX + "请求参数:" + ParametersUtils.getParameters(request));
    if (e instanceof MyException) {
      MyException myException = (MyException) e;
      log.error(AppConst.ERROR_LOG_PREFIX + myException.getMsg(), myException.getE());
      if (myException.getCode().equals(ResultEnum.SEARCH_PAGE_ERROR.getCode())) {
        JSONObject result = new JSONObject();
        result.put("code", myException.getCode());
        result.put("msg", myException.getMsg());
        return result;
      } else if (myException.getCode().equals(ResultEnum.ERROR_PAGE.getCode())) {
        resp.sendRedirect("/err");
        return "";
      } else {
        return new ResultInfo<>(myException.getCode(), myException.getMsg());
      }
    } else if (e instanceof UnauthorizedException) {
      resp.sendRedirect("/noauth");
      return "";
    } else {
      log.error(AppConst.ERROR_LOG_PREFIX + "错误信息:", e);
    }
    resp.sendRedirect("/err");
    return "";
  }

}

统一捕获异常之后,可以进行相应的处理,我这里没有进行特殊的处理,只是进行了一下区分,获取数据的接口抛出的异常,前台肯定是使用的ajax请求,因此返回前台一个json格式的信息,提示出错误内容。如果是跳转页面抛出的异常,类似404之类的,直接跳转到自定义的404页面。补充一点,springboot项目默认是有/error路由的,返回的就是error页面,所以,如果你在你的项目中定义一个error.html的页面,如果报404错误,会自动跳转到该页面。

补充,统一异常处理类中使用了一个注解@Slf4j,该注解是lombok包中的,项目中加入了该依赖后,再也不用写繁琐的get、set等代码,当然类似的像上边的声明log对象的代码也不用写了:

4. 日志配置文件区分环境

本示例使用的是logback日志框架。需要在resources目录中添加logback.xml配置文件,这是一个比较头疼的地方,我本来想一个配置文件也没有的,奈何我也不知道怎么将这个日志的配置文件放到数据库中,所以暂时先这么着了,好在几乎没有需要改动它的时候。

我在项目中添加了两个日志的配置文件,分别是logback-dev.xml和logback-pro.xml可以根据不同的环境决定使用哪个配置文件,在数据库配置表中(相当于写在了application.properties中)添加一条配置logging.config=classpath:logback-dev.xml来区分使用哪个文件作为日志的配置文件,配置文件内容如下:

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <property name="LOG_HOME" value="/Users/oven/log/demo"/>
  <!-- INFO日志定义 -->
  <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <File>${LOG_HOME}/demo.info.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <FileNamePattern>${LOG_HOME}/demo.info.%d{yyyy-MM-dd}.log</FileNamePattern>
      <maxHistory>180</maxHistory>
    </rollingPolicy>
    <encoder>
      <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- ERROR日志定义 -->
  <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <File>${LOG_HOME}/demo.error.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <FileNamePattern>${LOG_HOME}/demo.error.%d{yyyy-MM-dd}.log</FileNamePattern>
      <maxHistory>180</maxHistory>
    </rollingPolicy>
    <encoder>
      <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- DEBUG日志定义 -->
  <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <File>${LOG_HOME}/demo.debug.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <FileNamePattern>${LOG_HOME}/demo.debug.%d{yyyy-MM-dd}.log</FileNamePattern>
      <maxHistory>180</maxHistory>
    </rollingPolicy>
    <encoder>
      <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 定义控制台日志信息 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT"/>
  </root>
  <logger name="com.oven.controller" level="ERROR">
    <appender-ref ref="ERROR"/>
  </logger>
  <logger name="com.oven.exception" level="ERROR">
    <appender-ref ref="ERROR"/>
  </logger>
  <logger name="com.oven.mapper" level="DEBUG">
    <appender-ref ref="DEBUG"/>
  </logger>
  <logger name="com.oven.aop" level="INFO">
    <appender-ref ref="INFO"/>
  </logger>

</configuration>

在配置文件中,定义了三个级别的日志,info、debug和error分别输出到三个文件中,便于查看。在生成日志文件的时候,进行了按照日志进行拆分的配置,每一个级别的日志每一天都会重新生成一个,根据日期进行命名,超过180天的日志将自动会删除。当然你还可以按照日志大小进行拆分,我这里没有进行这项的配置。

5. 全局接口请求记录

进行全局的接口请求记录,可以记录接口的别调用情况,然后进行一些统计和分析,在本示例中,只是将全局的接口调用情况记录到了info日志中,没有进行相应的分析操作:

@Slf4j
@Aspect
@Component
public class WebLogAspect {

  @Pointcut("execution(public * com.oven.controller.*.*(..))")
  public void webLog() {
  }

  @Before("webLog()")
  public void doBefore() {
    // 获取请求
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    @SuppressWarnings("ConstantConditions") HttpServletRequest request = attributes.getRequest();
    // 记录请求内容
    log.info(AppConst.INFO_LOG_PREFIX + "请求地址:" + request.getRequestURL().toString());
    log.info(AppConst.INFO_LOG_PREFIX + "请求方法:" + request.getMethod());
    log.info(AppConst.INFO_LOG_PREFIX + "请求者IP:" + request.getRemoteAddr());
    log.info(AppConst.INFO_LOG_PREFIX + "请求参数:" + ParametersUtils.getParameters(request));
  }

  @AfterReturning(returning = "ret", pointcut = "webLog()")
  public void doAfterReturning(Object ret) {
    // 请求返回的内容
    if (ret instanceof ResultInfo) {
      log.info(AppConst.INFO_LOG_PREFIX + "返回结果:" + ((ResultInfo) ret).getCode().toString());
    }
  }

}

6. 集成shiro实现权限校验

集成shirl,轻松的实现了权限的管理,如果对shiro不熟悉朋友,还需要先把shiro入门一下才好,shiro的集成一般都需要自定义一个realm,来进行身份认证和授权,因此先来一个自定义realm:

MyShiroRealm.java

public class MyShiroRealm extends AuthorizingRealm {

  @Resource
  private MenuService menuService;
  @Resource
  private UserService userService;

  /**
   * 授权
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    User user = (User) principals.getPrimaryPrincipal();
    List<String> permissions = menuService.getAllMenuCodeByUserId(user.getId());
    authorizationInfo.addStringPermissions(permissions);
    return authorizationInfo;
  }

  /**
   * 身份认证
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String userName = String.valueOf(token.getUsername());
    // 从数据库获取对应用户名的用户
    User user = userService.getByUserName(userName);
    // 账号不存在
    if (user == null) {
      throw new UnknownAccountException(ResultEnum.NO_THIS_USER.getValue());
    }

    Md5Hash md5 = new Md5Hash(token.getPassword(), AppConst.MD5_SALT, 2);
    // 密码错误
    if (!md5.toString().equals(user.getPassword())) {
      throw new IncorrectCredentialsException(ResultEnum.PASSWORD_WRONG.getValue());
    }

    // 账号锁定
    if (user.getStatus().equals(1)) {
      throw new LockedAccountException(ResultEnum.USER_DISABLE.getValue());
    }
    ByteSource salt = ByteSource.Util.bytes(AppConst.MD5_SALT);
    return new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
  }

}

自定义完realm后需要一个配置文件但自定义的realm配置到shiro里:

ShiroConfig.java

@Configuration
public class ShiroConfig {

  @Bean
  public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    filterChainDefinitionMap.put("/static/**", "anon");
    filterChainDefinitionMap.put("/css/**", "anon");
    filterChainDefinitionMap.put("/font/**", "anon");
    filterChainDefinitionMap.put("/js/**", "anon");
    filterChainDefinitionMap.put("/*.js", "anon");
    filterChainDefinitionMap.put("/login", "anon");
    filterChainDefinitionMap.put("/doLogin", "anon");
    filterChainDefinitionMap.put("/**", "authc");
    shiroFilterFactoryBean.setLoginUrl("/login");
    shiroFilterFactoryBean.setSuccessUrl("/");
    shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
  }

  /**
   * 凭证匹配器
   */
  @Bean
  public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");
    hashedCredentialsMatcher.setHashIterations(2);
    return hashedCredentialsMatcher;
  }

  @Bean
  public MyShiroRealm myShiroRealm() {
    MyShiroRealm myShiroRealm = new MyShiroRealm();
    myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
    return myShiroRealm;
  }

  @Bean
  public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(myShiroRealm());
    return securityManager;
  }

  /**
   * 开启shiro aop注解
   */
  @Bean
  public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
  }

  @Bean(name = "simpleMappingExceptionResolver")
  public SimpleMappingExceptionResolver
  createSimpleMappingExceptionResolver() {
    SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
    Properties mappings = new Properties();
    mappings.setProperty("DatabaseException", "databaseError");
    mappings.setProperty("UnauthorizedException", "403");
    r.setExceptionMappings(mappings);
    r.setDefaultErrorView("error");
    r.setExceptionAttribute("ex");
    return r;
  }

}

身份认证如果简单的理解的话,你可以理解为登录的过程。授权就是授予你权利,代表你在这个系统中有权限做什么动作,具体shiro的内容小伙伴们自行去学习吧。

7. 登录校验,安全拦截

在集成了shiro之后,登录操作就需要使用到自定义的realm了,具体的登录代码如下:

/**
   * 登录操作
   *
   * @param userName 用户名
   * @param pwd   密码
   */
  @RequestMapping("/doLogin")
  @ResponseBody
  public Object doLogin(String userName, String pwd, HttpServletRequest req) throws MyException {
    try {
      Subject subject = SecurityUtils.getSubject();
      UsernamePasswordToken token = new UsernamePasswordToken(userName, pwd);
      subject.login(token);

      User userInDb = userService.getByUserName(userName);
      // 登录成功后放入application,防止同一个账户多人登录
      ServletContext application = req.getServletContext();
      @SuppressWarnings("unchecked")
      Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS);
      if (loginedMap == null) {
        loginedMap = new HashMap<>();
        application.setAttribute(AppConst.LOGINEDUSERS, loginedMap);
      }
      loginedMap.put(userInDb.getUserName(), req.getSession().getId());

      // 登录成功后放入session中
      req.getSession().setAttribute(AppConst.CURRENT_USER, userInDb);
      logService.addLog("登录系统!", "成功!", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req));
      return super.success("登录成功!");
    } catch (Exception e) {
      User userInDb = userService.getByUserName(userName);
      if (e instanceof UnknownAccountException) {
        logService.addLog("登录系统!", "失败[" + ResultEnum.NO_THIS_USER.getValue() + "]", 0, "", IPUtils.getClientIPAddr(req));
        return super.fail(ResultEnum.NO_THIS_USER.getCode(), ResultEnum.NO_THIS_USER.getValue());
      } else if (e instanceof IncorrectCredentialsException) {
        logService.addLog("登录系统!", "失败[" + ResultEnum.PASSWORD_WRONG.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req));
        return super.fail(ResultEnum.PASSWORD_WRONG.getCode(), ResultEnum.PASSWORD_WRONG.getValue());
      } else if (e instanceof LockedAccountException) {
        logService.addLog("登录系统!", "失败[" + ResultEnum.USER_DISABLE.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req));
        return super.fail(ResultEnum.USER_DISABLE.getCode(), ResultEnum.USER_DISABLE.getValue());
      } else {
        throw new MyException(ResultEnum.UNKNOW_ERROR.getCode(), "登录操作出错,请联系网站管理人员。", e);
      }
    }
  }

身份认证的操作交给了shiro,利用用户名和密码构造一个身份的令牌,调用shiro的login方法,这个时候就会进入自定义reaml的身份认证方法中,也就是上一步中的doGetAuthenticationInfo方法,具体的认证操作看上一步的代码,无非就是账号密码的校验等。身份认证的时候,通过抛出异常的方式给登录操作返回信息,从而在登录方法中判断身份认证失败后的信息,从而返回给前台进行提示。

在身份认证通过后,拿到当前登录用户的信息,首先放到session中,便于后续的使用。其次在放到application对象中,防止同一个账号的多次登录。

有了身份任何和授权自然就少不了安全校验,在本示例中使用了一个拦截器来实现安全校验的工作:

SecurityInterceptor.java

@Component
public class SecurityInterceptor extends HandlerInterceptorAdapter {

  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
    resp.setContentType("text/plain;charset=UTF-8");
    String servletPath = req.getServletPath();
    // 放行的请求
    if (servletPath.startsWith("/login") || servletPath.startsWith("/doLogin") || servletPath.equals("/err")) {
      return true;
    }
    if (servletPath.startsWith("/error")) {
      resp.sendRedirect("/err");
      return true;
    }

    // 获取当前登录用户
    User user = (User) req.getSession().getAttribute(AppConst.CURRENT_USER);

    // 没有登录状态下访问系统主页面,都跳转到登录页,不提示任何信息
    if (servletPath.startsWith("/")) {
      if (user == null) {
        resp.sendRedirect(getDomain(req) + "/login");
        return false;
      }
    }

    // 未登录或会话超时
    if (user == null) {
      String requestType = req.getHeader("X-Requested-With");
      if ("XMLHttpRequest".equals(requestType)) { // ajax请求
        ResultInfo<Object> resultInfo = new ResultInfo<>();
        resultInfo.setCode(ResultEnum.SESSION_TIMEOUT.getCode());
        resultInfo.setData(ResultEnum.SESSION_TIMEOUT.getValue());
        resp.getWriter().write(JSONObject.toJSONString(resultInfo));
        return false;
      }
      String param = URLEncoder.encode(ResultEnum.SESSION_TIMEOUT.getValue(), "UTF-8");
      resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param);
      return false;
    }

    // 检查是否被其他人挤出去
    ServletContext application = req.getServletContext();
    @SuppressWarnings("unchecked")
    Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS);
    if (loginedMap == null) { // 可能是掉线了
      String requestType = req.getHeader("X-Requested-With");
      if ("XMLHttpRequest".equals(requestType)) { // ajax请求
        ResultInfo<Object> resultInfo = new ResultInfo<>();
        resultInfo.setCode(ResultEnum.LOSE_LOGIN.getCode());
        resultInfo.setData(ResultEnum.LOSE_LOGIN.getValue());
        resp.getWriter().write(JSONObject.toJSONString(resultInfo));
        return false;
      }
      String param = URLEncoder.encode(ResultEnum.LOSE_LOGIN.getValue(), "UTF-8");
      resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param);
      return false;
    }
    String loginedUserSessionId = loginedMap.get(user.getUserName());
    String mySessionId = req.getSession().getId();

    if (!mySessionId.equals(loginedUserSessionId)) {
      String requestType = req.getHeader("X-Requested-With");
      if ("XMLHttpRequest".equals(requestType)) { // ajax请求
        ResultInfo<Object> resultInfo = new ResultInfo<>();
        resultInfo.setCode(ResultEnum.OTHER_LOGINED.getCode());
        resultInfo.setData(ResultEnum.OTHER_LOGINED.getValue());
        resp.getWriter().write(JSONObject.toJSONString(resultInfo));
        return false;
      }
      String param = URLEncoder.encode(ResultEnum.OTHER_LOGINED.getValue(), "UTF-8");
      resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param);
      return false;
    }
    return true;
  }

  /**
   * 获得域名
   */
  private String getDomain(HttpServletRequest request) {
    String path = request.getContextPath();
    return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path;
  }

}

在拦截器中,首先对一些不需要校验的请求进行放行,例如登录动作、登录页面请求以及错误页面等。然后获取当前登录的用户,如果没有登录则自动跳转到登录页面。在返回前台的时候,判断请求属于同步请求还是异步请求,如果是同步请求,直接进行页面的跳转,跳转到登录页面。如果是异步请求,则返回前台一个json数据,提示前台登录信息失效。这里补充一点,前台可以使用ajaxhook进行异步请求的捕获,相当于一个前端的全局拦截器,拦截所有的异步请求,可以监视所有异步请求的返回结果,如果返回的是登录失效,则进行跳转到登录页面的操作。具体ajaxhook的使用方法请自行学习,本示例中暂时没有使用。

下面是判断同一个账号有没有多次登录,具体方法就是使用当前的sessionId,将当前登录用户和请求sissionId作为一个key-value放到了application中,如果该用户的sessionId发生了变化,说明又有一个人登录了该账号,然后就进行相应的提示操作。

8. 配置虚拟路径

web项目中免不了并上传的操作,图片或者文件,如果上传的是图片,一般还要进行回显的操作,我们不想将上传的文件直接存放在项目的目录中,而是放在一个自定义的目录,同时项目还可以访问:

这样在进行上传操作的时候,就可以将上传的文件放到项目以外的目录中,然后外部访问的时候,通过虚拟路径进行映射访问。

9. 集成redis缓存

springboot的强悍就是集成一个东西太方便了,如果你不想做任何配置,只需要加入redis的依赖,然后在配置文件(本示例中配置是在数据库中)中添加redis的链接信息,就可以在项目中使用redis了。

本示例中使用redis做缓存,首先写了一个缓存的类,代码有些长不做展示。然后在service层进行缓存的操作:

代码中使用了double check的骚操作,防止高并发下缓存失效的问题(虽然我的示例不可能有高并发,哈哈)。另外就是缓存更新的问题,网上说的有很多,先更新数据再更新缓存,先更新缓存再更新数据库等等,具体要看你是做什么,本示例中没有什么需要特殊注意的地方,因此就先更新数据库,然后再移除缓存:

10. 项目代码和依赖以及静态资源分别打包

之前遇到一个问题,springboot打包之后是一个jar文件,如果将所有依赖也打到这个jar包中的话,那么这个jar包动辄几十兆,来回传输不说,如果想改动其中的一个配置内容,还异常的繁琐,因此,将项目代码,就是自己写的代码打成一个jar包(一般只有几百k),然后将所有的依赖打包到一个lib目录,然后再将所有的配置信息以及静态文件打包到resources目录,这样,静态文件可以直接进行修改,浏览器清理缓存刷新即可出现改动效果,而且打包出来的项目代码也小了很多,至于依赖,一般都是不变的,所以也没必要每次都打包它。具体操作就是在pom.xml中增加一个插件即可,代码如下:

代码太长,不做展示

11. 项目启动

到现在都没有贴一个项目的目录结构,先来一张。目录中项目跟目录下的demo.sh就是启动脚本,当时从网上抄袭改装过来的,源代码出自那位大师之手我就不知道了,先行谢过。在部署到服务器的时候,如果服务器上安装好了jdk、maven、git,每次修改完代码,直接git pull下来,然后mvn package打包,然后直接./demo.sh start就可以启动项目,方便快速。慢着,忘记了,如果你提交到github中的application.properties中的数据源配置信息是开发环境的话,那么你在打包之后,target/resources中的application.properties中的数据源需要改成开发环境才可以启动。当然如果你嫌麻烦,可以直接将开发环境的数据源配置push到github中,安不安全就要你自己考虑了。

12. 总结

示例中可能还有一些细节没有说到,总之这个项目是慢慢的添砖添瓦弄出来的,自己在写很多其他的项目的时候,都是以此项目为模板进行改造出来的,个人感觉很实用很方便,用着也很舒服。github地址:https://github.com/503612012/demo欢迎收藏。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • spring boot+thymeleaf+bootstrap实现后台管理系统界面

    最近在学spring boot ,学习一个框架无非也就是使用它来做以前做的事情,两者比较才有不同,说一下自己使用的体会. 先来说下spring boot ,微框架.快速开发,相当于零配置,从一个大神那看来的说:spring boot 相当于框架的框架 ,就是集成了很多,用哪个添加哪个的依赖就行,这样的话自己看不到配置,对于习惯了使用配置刚使用spring boot的开发者来说可能还有点不习惯,什么都不用配,看不到配置感觉对项目整体架构有点陌生,再说在spring boot 中使用 thymele

  • Spring Boot 开发私有即时通信系统(WebSocket)

    1/ 概述 利用Spring Boot作为基础框架,Spring Security作为安全框架,WebSocket作为通信框架,实现点对点聊天和群聊天. 2/ 所需依赖 Spring Boot 版本 1.5.3,使用MongoDB存储数据(非必须),Maven依赖如下: <properties> <java.version>1.8</java.version> <thymeleaf.version>3.0.0.RELEASE</thymeleaf.ve

  • Spring Boot 会员管理系统之处理文件上传功能

    温馨提示 Spring Boot会员管理系统的中,需要涉及到Spring框架,SpringMVC框架,Hibernate框架,thymeleaf模板引擎.所以,可以学习下这些知识.当然,直接入门的话使用是没问题,但是,涉及到一些异常和原理的话可能就有些困难. 1. 前端部分 在前端部分addMember.html是通过form表单来提交会员的信息,其中就包括了图片上传功能(这里涉及了文件上传操作),表单部分代码如下: <form th:action="@{/admin/addMember}

  • Spring Boot中使用 Spring Security 构建权限系统的示例代码

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作. 权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重.在Spring Boot中使用 Spring Security 构建权限系统是非常轻松和简单的.下面我们就来快速入门 Spring Security .在开始前我们需要一对

  • 详解在spring boot中消息推送系统设计与实现

    推送系统作为通用的组件,存在的价值主要有以下几点 会被多个业务项目使用,推送系统独立维护可降低维护成本 推送系统一般都是调用三方api进行推送,三方api一般会有调用频率/次数限制,被推送的消息需要走队列来合理调用三方api,控制调用的频率和次数 业务无关,一般推送系统设计成不需要关心业务逻辑 核心技术 消息队列 三方服务api调用 安卓app推送 苹果app推送 微信小程序推送 邮件推送 钉钉推送 短信推送 消息队列选用阿里云提供的rocketmq,官方文档:https://help.aliy

  • Spring Boot与Spark、Cassandra系统集成开发示例

    本文演示以Spark作为分析引擎,Cassandra作为数据存储,而使用Spring Boot来开发驱动程序的示例. 1.前置条件 安装Spark(本文使用Spark-1.5.1,如安装目录为/opt/spark) 安装Cassandra(3.0+) 创建keyspace CREATE KEYSPACE hfcb WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }; 创建table CREATE

  • 如何通过SpringBoot实现商城秒杀系统

    这篇文章主要介绍了如何通过SpringBoot实现商城秒杀系统,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 学习自:地址 1.主要流程 1.1数据库: 1.2 环境 window下:Zookeeper,Redis,rabbitmq-server.jdk1.8以上. 1.3 介绍 这里只做秒杀部分功能,其他功能不会涉及.项目运行后可访问秒杀商品页面 当用户没登陆,点击详情会跳转到登陆页面. 用户登陆后可以查看商品的详情并进行抢购. 注意,用户对

  • SpringBoot+WebSocket搭建简单的多人聊天系统

    前言 今天闲来无事,就来了解一下WebSocket协议.来简单了解一下吧. WebSocket是什么 首先了解一下WebSocket是什么?WebSocket是一种在单个TCP连接上进行全双工通信的协议.这是一种比较官方的说法,简单点来说就是,在一次TCP连接中,通信的双方可以相互通信.比如A和B在打电话,A说话的时候,B也可以说话来进行信息的交互,这就叫做全双工通信.对应的是单工通信,和半双工通信,单工通信就是只能由A向B通信,比如电脑和打印机.半双工通信是可以AB可以互相通信,但是同一时间只

  • SpringBoot跨系统单点登陆的实现方法

    什么是单点登陆 单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性.当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录.这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中.相同的,单一退出(single sign-off)就是指,只需要单一的退出动作,就可以结束对于多个系统的访问权限. 单点登陆带来的好处 降低访问

  • 基于springboot搭建的web系统架构的方法步骤

    从接触springboot开始,便深深的被它的简洁性深深的折服了,精简的配置,方便的集成,使我再也不想用传统的ssm框架来搭建项目,一大堆的配置文件,维护起来很不方便,集成的时候也要费力不少.从第一次使用springboot开始,一个简单的main方法,甚至一个配置文件也不需要(当然我是指的没有任何数据交互,没有任何组件集成的情况),就可以把一个web项目启动起来,下面总结一下自从使用springboot依赖,慢慢完善的自己的一个web系统的架构,肯定不是最好的,但平时自己用着很舒服. 1. 配

  • 基于Docker搭建ELK 日志系统的方法

    背景需求: 业务发展越来越庞大,服务器越来越多 各种访问日志.应用日志.错误日志量越来越多,导致运维人员无法很好的去管理日志 开发人员排查问题,需要到服务器上查日志,不方便 运营人员需要一些数据,需要我们运维到服务器上分析日志. ELK介绍: ELK是三个开源软件的缩写,分别为:Elasticsearch . Logstash以及Kibana , 它们都是开源软件. 目前由于原本的ELK Stack成员中加入了 Beats 工具所以已改名为Elastic Stack. Beats,它是一个轻量级

  • Linux搭建C++开发调试环境的方法步骤

    安装g++ Linux编译C++程序必须安装g++编译器.这里使用yum方式安装.首先切换到root账号,su - root 然后输入密码. 执行yum install gcc-c++(注意不是yum install g++),报错. 报错是因为yum需要配置正确的服务器地址,服务器是提供yum安装包的,也被称作yum源.配置yum源的配置文件在/etc/yum.repos.d/目录下,可以看到系统自带了两个文件. cat 文件名称,会打印文件全部内容.可以看到两个文件要么没配置,要么地址是无法

  • 手把手搭建Java共享网盘的方法步骤

    项目介绍 在线共享网盘采用jsp+servlet搭建项目结构实现共享网盘,项目分为管理员,普通用户和付费用户三种角色,根据不同角色控制不同权限,实现不同用户对个人文件文件,所有文件,共享文件的增删改查操作. 项目适用人群 正在做毕设的学生,或者需要项目实战练习的Java学习者 开发环境: jdk 8 intellij idea tomcat 8.5.40 mysql 5.7 所用技术: jsp+servlet js+ajax layUi jdbc直连 项目访问地址 http://localhos

  • SpringBoot项目中使用redis缓存的方法步骤

    本文介绍了SpringBoot项目中使用redis缓存的方法步骤,分享给大家,具体如下: Spring Data Redis为我们封装了Redis客户端的各种操作,简化使用. - 当Redis当做数据库或者消息队列来操作时,我们一般使用RedisTemplate来操作 - 当Redis作为缓存使用时,我们可以将它作为Spring Cache的实现,直接通过注解使用 1.概述 在应用中有效的利用redis缓存可以很好的提升系统性能,特别是对于查询操作,可以有效的减少数据库压力. 具体的代码参照该

  • SpringBoot通过RedisTemplate执行Lua脚本的方法步骤

    lua 脚本 Redis 中使用 lua 脚本,我们需要注意的是,从 Redis 2.6.0后才支持 lua 脚本的执行. 使用 lua 脚本的好处: 原子操作:lua脚本是作为一个整体执行的,所以中间不会被其他命令插入. 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延. 复用性:lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码量. 1.RedisScript 首先你得引入spring-boot-starter-data-redis依赖,其

  • IDEA中将SpringBoot项目提交到git仓库的方法步骤

    这里用一个博客项目提交到gitee作为演示: 1.通过网站右上角的「+」号,选择「新建仓库」,进入新建仓库页面 2.填写相关信息,下面的三个选项都不要勾 3.在打开的[Create Git Repository]对话框内选择本地仓库的位置,这里我选择项目的根目录 4.右击项目点击[Git]→[Add],接着点击[Git]→ [Commit Directory]在打开的窗口中选择要上传到本地仓库的代码并添加注释后提交到本地仓库内 5.右击项目点击[Git]→[Manage Remotes].在打开

  • golang搭建静态web服务器的实现方法

    我胡汉三又回来啦.好久没发文了,为保持平台上的活跃度,我今天就分享下个刚学到的知识,使用golang搭建静态web服务器,亲测可用,附代码! 使用过golang语言的程序猿都应该知道,在使用golang开发的时候,我们是不需要诸如iis,apache,nginx,kangle等服务器支持的. 为什么呢? 原因是,golang的net/http包中已经提供了HTTP的客户端与服务端实现方案. 网上言论都说golang不适合做web开发,相对php.java..net.nodejs等各类后端语言来说

  • Springboot如何实现Web系统License授权认证

    在我们做系统级框架的时候,我们要一定程度上考虑系统的使用版权,不能随便一个人拿去在任何环境都能用,所以我们需要给我们系统做一个授权认证机制,只有上传了我们下发的lic文件并验证通过,才能正常使用,下面就开始一步一步实现这个功能 1.生成机器码 我们首先要做的就是对软件部署的环境的唯一性进行限制,这里使用的是macadderss,当然你也可以换成cpu序列编号,并无太大影响,先上代码 private static String getMac() { try { Enumeration<Networ

  • 利用docker搭建web服务环境的方法步骤

    前言 做过开发的人对开发环境的安装.配置应该都不会太陌生,不管你做什么开发,对开发环境都会具有一定的依赖性的.对于 PHP 的 Web 开发来说,开发环境至少要有一个 Web 服务器(如 Apache.Nginx).一个数据库服务器(如 MySQL)和一个 PHP 解析器吧,如果你的 PHP 代码使用的一些扩展,那你还需要给你的 PHP 安装上这些扩展,那样你的 PHP 代码才能够顺利地跑起来.对于一个只想静静地敲代码的开发人员来说,开发环境的安装与配置这种事情,只要经历一次就已经很折腾人了.可

随机推荐