基於springboot的web項目最佳實踐

springboot 能夠說是如今作javaweb開發最火的技術,我在基於springboot搭建項目的過程當中,踩過很多坑,發現整合框架時並不是僅僅引入starter 那麼簡單。html

要作到簡單,易用,擴展性更好,還需作很多二次封裝,因而便寫了個基於springboot的web項目腳手架,對一些經常使用的框架進行整合,並進行了簡單的二次封裝。前端

項目名baymax取自動畫片超能陸戰隊裏面的大白,大白是一個醫護充氣機器人,但願這個項目你能像大白同樣貼心,能夠減小你的工做量。java

github https://github.com/zhaoguhong/baymaxmysql

web

web模塊是開發web項目必不可少的一個模塊git

maven 依賴github

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

對於先後端分離項目,推薦直接使用@RestController註解
須要注意的是,不建議直接用RequstMapping註解而且不指定方法類型的寫法,推薦使用GetMaping或者PostMaping之類的註解web

@SpringBootApplication
@RestController
public class BaymaxApplication {

  public static void main(String[] args) {
    SpringApplication.run(BaymaxApplication.class, args);
  }

  @GetMapping("/test")
  public String test() {
    return "hello baymax";
  }
}

單元測試

spring 對單元測試也提供了很好的支持ajax

maven 依賴redis

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

添加 @RunWith(SpringRunner.class)@SpringBootTest 便可進行測試spring

@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
}

對於Controller層的接口,能夠直接用MockMvc進行測試

@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {

  @Autowired
  private WebApplicationContext context;
  private MockMvc mvc;

  @Before
  public void setUp() throws Exception {
    mvc = MockMvcBuilders.webAppContextSetup(context).build();
  }

  @Test
  public void testValidation() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/test"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.content().string("hello baymax"));
  }

}

actuator應用監控

actuator 是 spring 提供的應用監控功能,經常使用的配置項以下

# actuator端口 默認應用端口
management.server.port=8082
# 加載全部的端點 默認只加載 info,health
management.endpoints.web.exposure.include=*
# actuator路徑前綴,默認 /actuator
management.endpoints.web.base-path=/actuator

lombok

lombok能夠在編譯期生成對應的java代碼,使代碼看起來更簡潔,同時減小開發工做量

用lombok後的實體類

@Data
public class Demo {
  private Long id;
  private String userName;
  private Integer age;
}

須要注意,@Data 包含 @ToString、@Getter、@Setter、@EqualsAndHashCode、@RequiredArgsConstructorRequiredArgsConstructor 並非無參構造,無參構造的註解是NoArgsConstructor

RequiredArgsConstructor 會生成 會生成一個包含常量(final),和標識了@NotNull的變量 的構造方法

baseEntity

把表中的基礎字段抽離出來一個BaseEntity,全部的實體類都繼承該類

/**
 * 實體類基礎類
 */
@Data
public abstract class BaseEntity implements Serializable {
  /**
   * 主鍵id
   */
  private Long id;
  /**
   * 建立人
   */
  private Long createdBy;
  /**
   * 建立時間
   */
  private Date createdTime;
  /**
   * 更新人
   */
  private Long updatedBy;
  /**
   * 更新時間
   */
  private Date updatedTime;
  /**
   * 是否刪除
   */
  private Integer isDeleted;

}

統一響應返回值

先後端分離項目基本上都是ajax調用,因此封裝一個統一的返回對象有利於前端統一處理

/**
 * 用於 ajax 請求的響應工具類
 */
@Data
public class ResponseResult<T> {
  // 未登陸
  public static final String UN_LOGIN_CODE = "401";
  // 操做失敗
  public static final String ERROR_CODE = "400";
  // 服務器內部執行錯誤
  public static final String UNKNOWN_ERROR_CODE = "500";
  // 操做成功
  public static final String SUCCESS_CODE = "200";
  // 響應信息
  private String msg;
  // 響應code
  private String code;
  // 操做成功,響應數據
  private T data;

  public ResponseResult(String code, String msg, T data) {
    this.msg = msg;
    this.code = code;
    this.data = data;
  }
}

返回給前端的值用ResponseResult包裝一下

/**
   * 測試成功的 ResponseResult
   */
  @GetMapping("/successResult")
  public ResponseResult<List<Demo>> test() {
    List<Demo> demos = demoMapper.getDemos();
    return ResponseResult.success(demos);
  }

  /**
   * 測試失敗的 ResponseResult
   */
  @GetMapping("/errorResult")
  public ResponseResult<List<Demo>> demo() {
    return ResponseResult.error("操做失敗");
  }

ResponseEntity

spring其實封裝了ResponseEntity 處理響應,ResponseEntity 包含 狀態碼,頭部信息,響應體 三部分

/**
   * 測試請求成功
   * @return
   */
  @GetMapping("/responseEntity")
  public ResponseEntity<String> responseEntity() {
    return ResponseEntity.ok("請求成功");
  }

  /**
   * 測試服務器內部錯誤
   * @return
   */
  @GetMapping("/InternalServerError")
  public ResponseEntity<String> responseEntityerror() {
    return new ResponseEntity<>("出錯了", HttpStatus.INTERNAL_SERVER_ERROR);
  }

異常

自定義異常體系

爲了方便異常處理,定義一套異常體系,BaymaxException 作爲全部自定義異常的父類

// 項目全部自定義異常的父類
public class BaymaxException extends RuntimeException
// 業務異常 該異常的信息會返回給用戶
public class BusinessException  extends BaymaxException
// 用戶未登陸異常
public class NoneLoginException  extends BaymaxException

全局異常處理

對全部的異常處理後再返回給前端

@RestControllerAdvice
public class GlobalControllerExceptionHandler {

  /**
   * 業務異常
   */
  @ExceptionHandler(value = {BusinessException.class})
  public ResponseResult<?> handleBusinessException(BusinessException ex) {
    String msg = ex.getMessage();
    if (StringUtils.isBlank(msg)) {
      msg = "操做失敗";
    }
    return ResponseResult.error(msg);
  }

  /**
   * 處理未登陸異常
   */
  @ExceptionHandler(value = {NoneLoginException.class})
  public ResponseResult<?> handleNoneLoginException(NoneLoginException ex) {
    return ResponseResult.unLogin();
  }

異常持久化

對於未知的異常,保存到數據庫,方便後續排錯

須要說明是的,若是項目訪問量比較大,推薦用 ELK 這種成熟的日誌分析系統,不推薦日誌保存到關係型數據庫

@Autowired
  private ExceptionLogMapper exceptionLogMapper;
  /**
   * 處理未知的錯誤
   */
  @ExceptionHandler(value = {Exception.class})
  public ResponseResult<Long> handleunknownException(Exception ex) {
    ExceptionLog log = new ExceptionLog(new Date(), ExceptionUtils.getStackTrace(ex));
    exceptionLogMapper.insert(log);
    ResponseResult<Long> result = ResponseResult.unknownError("服務器異常:" + log.getId());
    result.setData(log.getId());
    return result;
  }

異常日誌接口

對外開一個異常日誌查詢接口/anon/exception/{異常日誌id},方便查詢

@RestController
@RequestMapping("/anon/exception")
public class ExceptionController {

  @Autowired
  private ExceptionLogMapper exceptionLogMapper;

  @GetMapping(value = "/{id}")
  public String getDemo(@PathVariable(value = "id") Long id) {
    return exceptionLogMapper.selectByPrimaryKey(id).getException();
  }

}

數據校驗

JSR 303定義了一系列的 Bean Validation 規範,Hibernate Validator 是 Bean Validation 的實現,並進行了擴展

spring boot 使用 也很是方便

public class Demo extends BaseEntity{
  @NotBlank(message = "用戶名不容許爲空")
  private String userName;
  @NotBlank
  private String title;
  @NotNull
  private Integer age;
}

參數前面添加@Valid註解便可

@PostMapping("/add")
  public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
    demoMapper.insert(demo);
    return ResponseResult.success();
  }

對於校驗結果能夠每一個方法單獨處理,若是不處理,會拋出有異常,能夠對校驗的異常作全局處理

GlobalControllerExceptionHandler 添加

/**
   * 處理校驗異常
   */
  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseResult<?> handleValidationException(MethodArgumentNotValidException ex) {
    BindingResult result = ex.getBindingResult();
    if (result.hasErrors()) {
      StringJoiner joiner = new StringJoiner(",");
      List<ObjectError> errors = result.getAllErrors();
      errors.forEach(error -> {
        FieldError fieldError = (FieldError) error;
        joiner.add(fieldError.getField() + " " + error.getDefaultMessage());
      });
      return ResponseResult.error(joiner.toString());
    } else {
      return ResponseResult.error("操做失敗");
    }
  }

log

spring boot 的默認使用的日誌是logback,web模塊依賴的有日誌 starter,因此這裏不用再引入依賴,詳細配置

修改日誌級別

Actuator 組件提供了日誌相關接口,能夠查詢日誌級別或者動態修改日誌級別

// 查看全部包/類的日誌級別
/actuator/loggers
// 查看指定包/類日誌級別 get 請求
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
//修改日誌級別 post 請求 參數 {"configuredLevel":"debug"}
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController

日誌切面

添加一個日誌切面,方便記錄方法執行的入參和出參

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAspect {

  /**
   * 日誌描述
   */
  String value() default "";

  /**
   * 日誌級別
   */
  String level() default "INFO";

}

使用時直接添加到方法上便可

swagger

swagger 是一個很好用的文檔生成工具

maven 依賴

<dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.7.0</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.7.0</version>
    </dependency>

相關配置

@Configuration
@EnableSwagger2
public class SwaggerConfig {

  @Value("${swagger.enable:false}")
  private boolean swaggerEnable;

  //文檔訪問前綴
  public static final String ACCESS_PREFIX = "/swagger-resources/**,/swagger-ui.html**,/webjars/**,/v2/**";

  @Bean
  public Docket docket() {
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        // 設置是否開啓swagger,生產環境關閉
        .enable(swaggerEnable)
        .select()
        // 當前包路徑
        .apis(RequestHandlerSelectors.basePackage("com.zhaoguhong.baymax"))
        .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
        .paths(PathSelectors.any())
        .build();
  }

  // 構建api文檔的詳細信息
  private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
        // 頁面標題
        .title("接口文檔")
        // 建立人
        .contact(new Contact("孤鴻", "https://github.com/zhaoguhong/baymax", ""))
        // 版本號
        .version("1.0")
        // 描述
        .description("大白的接口文檔")
        .build();
  }
}

爲文檔加一個開關

#是否開啓swagger文檔,生產環境關閉
swagger.enable=true

而後就能夠愉快的在寫代碼的同時寫文檔了

@PostMapping("/add")
  @ApiOperation(value = "新增 demo")
  public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
    demoMapper.insert(demo);
    return ResponseResult.success();
  }
@ApiModel("示例")
public class Demo extends BaseEntity{
  @ApiModelProperty("用戶名")
  private String userName;
  @ApiModelProperty("標題")
  private String title;
  @ApiModelProperty("年齡")
  private Integer age;
}

訪問 localhost:8080/swagger-ui.html 就能夠看到效果了

數據庫鏈接池

springboot1.X的數據庫鏈接池是tomcat鏈接池,springboot2默認的數據庫鏈接池由Tomcat換成 HikariCPHikariCP是一個高性能的JDBC鏈接池,號稱最快的鏈接池

Druid 是阿里巴巴數據庫事業部出品,爲監控而生的數據庫鏈接池,這裏選取Druid做爲項目的數據庫鏈接池

maven 依賴

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>

設置用戶名密碼

spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456

而後就能夠訪問 localhost:8080/druid看監控信息了

spring jdbc

maven 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

spring對jdbc作了封裝和抽象,最經常使用的是 jdbcTemplateNamedParameterJdbcTemplate兩個類,前者使用佔位符,後者使用命名參數,我在jdbcDao作了一層簡單的封裝,提供統一的對外接口

jdbcDao主要方法以下:

// 佔位符
find(String sql, Object... args)
// 佔位符,手動指定映射mapper
find(String sql, Object[] args, RowMapper<T> rowMapper)
// 命名參數
find(String sql, Map<String, ?> paramMap)
// 命名參數,手動指定映射mapper
find(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
//springjdbc 原queryForMap方法,若是沒查詢到會拋異常,此處若是沒有查詢到,返回null
queryForMap(String sql, Object... args)
queryForMap(String sql, Map<String, ?> paramMap)
// 分頁查詢
find(Page<T> page, String sql, Map<String, ?> parameters, RowMapper<?> mapper)
// 分頁查詢
find(Page<T> page, String sql, RowMapper<T> mapper, Object... args)

jpa

jpajava 持久化的標準,spring data jpa 使操做數據庫變得更方便,須要說明的 spring data jpa 自己並非jpa的實現,它默認使用的 providerhibernate

maven 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

我把通用的方法抽取了出來了,封裝了一個BaseRepository,使用時,直接繼承該接口便可

public interface DemoRepository extends BaseRepository<Demo> {

}

BaseRepository 主要方法以下

// 新增,會對建立時間,建立人自動賦值
void saveEntity(T entity)
// 更新,會對更新時間,更新人自動賦值
void updateEntity(T entity)
// 邏輯刪除
void deleteEntity(T entity)
// 批量保存
void saveEntites(Collection<T> entitys)
// 批量更新
void updateEntites(Collection<T> entitys)
// 批量邏輯刪除
void deleteEntites(Collection<T> entitys)
// 根據id獲取實體,會過濾掉邏輯刪除的
T getById(Long id)

若是想使用傳統的sql形式,能夠直接使用JpaDao,爲了方便使用,我儘可能使JpaDao和JdbcDao的接口保持統一

JpaDao主要方法以下

// 佔位符 例如:from Demo where id =?
find(String sql, Object... args)
// 命名參數
find(String sql, Map<String, ?> paramMap)
// 分頁
find(Page<T> page, String hql, Map<String, ?> parameters)
// 分頁
find(Page<T> page, String hql, Object... parameters)

redis

Redis 是性能極佳key-value數據庫,經常使用來作緩存,
java 中經常使用的客戶端有 JedisLettuce, spring data redis 是基於 Lettuce 作的二次封裝

maven 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

爲了在redis讀起來更方便,更改序列化方式

@Configuration
public class RedisConfig {

  /**
   * 設置序列化方式
   */
  @Bean
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setKeySerializer(RedisSerializer.string());
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
    redisTemplate.setHashKeySerializer(RedisSerializer.string());
    redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
    return redisTemplate;
  }

  @Bean
  public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    // 將類名稱序列化到json串中
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
        new Jackson2JsonRedisSerializer<Object>(Object.class);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    return jackson2JsonRedisSerializer;
  }
}

spring cache

spring cache 抽象出了一組緩存接口,經過註解的方式使用,能夠很方便的配置其具體實現,詳細配置

這裏使用redis作爲緩存 provider, 默認的value序列化方式是JDK,爲方便查看,能夠修改成用json序列化

有時會有設置redis key前綴的需求,默認是這樣的

static CacheKeyPrefix simple() {
        // 在 cacheName 後面添加 "::"
        return name -> name + "::";
    }

spring boot 提供的有配置前綴的屬性

spring.cache.redis.key-prefix= # Key prefix.

但這是一個坑,這樣寫的效果實際這樣的,會把cacheName幹掉,顯然不是咱們想要的

CacheKeyPrefix cacheKeyPrefix = (cacheName) -> prefix;

咱們想要的是把前綴加在最前面,保留cacheName

CacheKeyPrefix cacheKeyPrefix = (cacheName) -> keyPrefix + "::" + cacheName + "::";

參考org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration,聲明 RedisCacheManager

@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
public class SpringCacheConfig {

  @Autowired
  private CacheProperties cacheProperties;

  @Bean
  public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheManagerBuilder builder = RedisCacheManager
        .builder(redisConnectionFactory)
        .cacheDefaults(determineConfiguration());
    List<String> cacheNames = this.cacheProperties.getCacheNames();
    if (!cacheNames.isEmpty()) {
      builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
    }
    return builder.build();
  }

  private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration() {
    Redis redisProperties = this.cacheProperties.getRedis();
    org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
        .defaultCacheConfig();
    // 修改序列化爲json
    config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
        .fromSerializer(jackson2JsonRedisSerializer()));
    if (redisProperties.getTimeToLive() != null) {
      config = config.entryTtl(redisProperties.getTimeToLive());
    }
    if (redisProperties.getKeyPrefix() != null) {
      // 重寫前綴拼接方式
      config = config.computePrefixWith((cacheName) -> redisProperties.getKeyPrefix() + "::" + cacheName + "::");
    }
    if (!redisProperties.isCacheNullValues()) {
      config = config.disableCachingNullValues();
    }
    if (!redisProperties.isUseKeyPrefix()) {
      config = config.disableKeyPrefix();
    }
    return config;
  }
    // 省略 jackson2JsonRedisSerializer() 

}

mogodb

MongoDB 是文檔型數據庫,使用 spring data mogodb 能夠很方便對mogodb進行操做

maven 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>

sprign data mogodb 提供了 MongoTemplate 對mogodb進行操做,我在該類的基礎上又擴展了一下,能夠自定義本身的方法

@Configuration
public class MongoDbConfig {

  /**
   * 擴展本身的mogoTemplate
   */
  @Bean
  public MyMongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,
      MongoConverter converter) {
    return new MyMongoTemplate(mongoDbFactory, converter);
  }

}

我擴展了一個分頁的方法,能夠根據本身的狀況擴展其它方法

// mogodb 分頁
public <T> Page<T> find(Page<T> page, Query query, Class<T> entityClass)

mybatis

maven 依賴

<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
    </dependency>

經常使用配置以下

# 配置文件位置 classpath後面要加*,否則後面通配符無論用
mybatis.mapperLocations=classpath*:com/zhaoguhong/baymax/*/mapper/*Mapper.xml
# 開啓駝峯命名自動映射
mybatis.configuration.map-underscore-to-camel-case=true

dao層直接用接口,簡潔,方便

@Mapper
public interface DemoMapper {
  /**
   * 註解方式
   */
  @Select("SELECT * FROM demo WHERE user_name = #{userName}")
  List<Demo> findByUserName(@Param("userName") String userName);
  /**
   * xml方式
   */
  List<Demo> getDemos();
}

須要注意,xml的namespace必須是mapper類的全限定名,這樣才能夠創建dao接口與xml的關係

<mapper namespace="com.zhaoguhong.baymax.demo.dao.DemoMapper">
  <select id="getDemos" resultType="com.zhaoguhong.baymax.demo.entity.Demo">
        select * from demo
    </select>
</mapper>

通用mapper

mybatis 的單表增刪改查寫起來很囉嗦,通用mapper很好的解決了這個問題

maven 依賴

<dependency>
      <groupId>tk.mybatis</groupId>
      <artifactId>mapper-spring-boot-starter</artifactId>
      <version>1.2.4</version>
    </dependency>

經常使用配置以下

# 通用mapper 多個接口時用逗號隔開
mapper.mappers=com.zhaoguhong.baymax.mybatis.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL

定義本身的 MyMapper 方便擴展,MyMapper 接口 中封裝了通用的方法,和jpaBaseRepository相似,這裏再也不贅述

聲明mapper須要加Mapper註解,還稍顯麻煩,能夠用掃描的方式

@Configuration
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
public class MybatisConfig {
}

使用時直接繼承MyMapper接口便可

public interface DemoMapper extends MyMapper<Demo>{
}

分頁

pagehelper是一個很好用的mybatis的分頁插件

maven 依賴

<dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
      <version>1.2.3</version>
    </dependency>

經常使用配置以下

#pagehelper
#指定數據庫類型
pagehelper.helperDialect=mysql
#分頁合理化參數
pagehelper.reasonable=true

使用分頁

PageHelper.startPage(1, 5);
    List<Demo> demos = demoMapper.selectAll();

pagehelper 還有好多玩法,能夠參考這裏

自定義分頁

pagehelper 雖然好用,但項目中有本身的分頁對象,因此單獨寫一個攔截器,把他們整合到一塊兒
這個地方要特別注意插件的順序不要搞錯

@Configuration
// 設置mapper掃描的包
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
@Slf4j
public class MybatisConfig {

  @Autowired
  private List<SqlSessionFactory> sqlSessionFactoryList;

  /**
   * 添加自定義的分頁插件,pageHelper 的分頁插件PageInterceptor是用@PostConstruct添加的,自定義的應該在其後面添加
   * 真正執行時順序是反過來,先執行MyPageInterceptor,再執行 PageInterceptor
   *
   * 因此要保證 PageHelperAutoConfiguration 先執行
   */
  @Autowired
  public void addPageInterceptor(PageHelperAutoConfiguration pageHelperAutoConfiguration) {
    MyPageInterceptor interceptor = new MyPageInterceptor();
    for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
      sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
      log.info("註冊自定義分頁插件成功");
    }
  }

}

在使用時只須要傳入自定義的分頁對象便可

Page<Demo> page = new Page<>(1, 10);
    demos = demoMapper.getDemos(page);

spring security

安全模塊是項目中必不可少的一環,經常使用的安全框架有shirospring security,shiro相對輕量級,使用很是靈活,spring security相對功能更完善,並且能夠和spring 無縫銜接。這裏選取spring security作爲安全框架

maven 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

繼承WebSecurityConfigurerAdapter類就能夠進行配置了

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private SecurityProperties securityProperties;

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    http
        .authorizeRequests()
        // 設置能夠匿名訪問的url
        .antMatchers(securityProperties.getAnonymousArray()).permitAll()
        // 其它全部請求都要認證
        .anyRequest().authenticated()
        .and()
        .formLogin()
        // 自定義登陸頁
        .loginPage(securityProperties.getLoginPage())
        // 自定義登陸請求路徑
        .loginProcessingUrl(securityProperties.getLoginProcessingUrl())
        .permitAll()
        .and()
        .logout()
        .permitAll();

    // 禁用CSRF
    http.csrf().disable();
  }


  @Override
  public void configure(WebSecurity web) throws Exception {
    String[] ignoringArray = securityProperties.getIgnoringArray();
    // 忽略的資源,直接跳過spring security權限校驗
    if (ArrayUtils.isNotEmpty(ignoringArray)) {
      web.ignoring().antMatchers(ignoringArray);
    }
  }

  /**
   *
   * 聲明密碼加密方式
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
    auth.userDetailsService(userDetailsService)
        // 配置密碼加密方式,也能夠不指定,默認就是BCryptPasswordEncoder
        .passwordEncoder(passwordEncoder());
  }


}

實現UserDetailsService接口,定義本身的UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserRepository userRepository;

  @Override
  @Transactional
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsernameAndIsDeleted(username, SystemConstants.UN_DELETED);

    if (user == null) {
      throw new UsernameNotFoundException("username Not Found");
    }
    return user;
  }

}

配置項

#匿名訪問的url,多個用逗號分隔
security.anonymous=/test
#忽略的資源,直接跳過spring security權限校驗,通常是用作靜態資源,多個用逗號分隔
security.ignoring=/static/**,/images/**
#自定義登陸頁面
security.loginPage=/login.html
#自定義登陸請求路徑
security.loginProcessingUrl=/login

項目上下文

爲了方面使用,封裝一個上下文對象 ContextHolder

// 獲取當前線程HttpServletRequest
getRequest()
// 獲取當前線程HttpServletResponse
getResponse()
// 獲取當前HttpSession
getHttpSession()
setSessionAttribute(String key, Serializable entity)
getSessionAttribute(String key)
setRequestAttribute(String key, Object entity)
getRequestAttribute(String key)
// 獲取 ApplicationContext
getApplicationContext()
//根據beanId獲取spring bean
getBean(String beanId)
// 獲取當前登陸用戶
getLoginUser()
// 獲取當前登陸用戶 id
getLoginUserId()
// 獲取當前登陸用戶 爲空則拋出異常
getRequiredLoginUser()
// 獲取當前登陸用戶id, 爲空則拋出異常
getRequiredLoginUserId()

單點登陸

單點登陸系統(SSO,single sign-on)指的的,多個系統,共用一套用戶體系,只要登陸其中一個系統,訪問其餘系統不須要從新登陸

CAS

CAS(Central Authentication Service)是耶魯大學的一個開源項目,是比較流行的單獨登陸解決方案。在CAS中,只負責登陸的系統被稱爲服務端,其它全部系統被稱爲客戶端

登陸流程

  1. 用戶訪問客戶端,客戶端判斷是否登陸,若是沒有登陸,重定向到服務端去登陸
  2. 服務端登陸成功,帶着ticket重定向到客戶端
  3. 客戶端拿着ticket發送請求到服務端換取用戶信息,獲取到後就表示登陸成功

登出流程

跳轉到sso認證中心進行統一登出,cas 會通知全部客戶端進行登出

spring security 整合 cas

maven 依賴

<dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-cas</artifactId>
    </dependency>

spring security 對 cas 作了很好的封裝,在使用的過程當中,只須要定義好對應的登陸fifter和登出fifter便可,整合cas的代碼我寫在了WebSecurityConfig類中

相關屬性配置

#是否開啓單點登陸
cas.enable = true
#服務端地址
cas.serverUrl=
#客戶端地址
cas.clientUrl=
#登陸地址
cas.loginUrl=${cas.serverUrl}/login
#服務端登出地址
cas.serverLogoutUrl=${cas.serverUrl}/logout
#單點登陸成功回調地址
cas.clientCasUrl=${cas.clientUrl}/login/cas

郵件

由於要使用freeMarker解析模板,因此也要引入freeMarker依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>

    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
    </dependency>

相關配置

#郵件
#設置郵箱主機,163郵箱爲smtp.163.com,qq爲smtp.qq.com
spring.mail.host = smtp.163.com
spring.mail.username =
#受權碼
spring.mail.password =
#默認的郵件發送人
mail.sender =

封裝一個 MailService

// 根據相關配置發送郵件
  void sendMail(MailModel mailModel);
   // 發送簡單的郵件
  void sendSimleMail(String to, String subject, String content);
   // 發送html格式的郵件
  void sendHtmlMail(String to, String subject, String content);
   // 發送帶附件的郵件
  void sendAttachmentMail(String to, String subject, String content, String path);
   // 發送帶附件的html格式郵件
  void sendAttachmentHtmlMail(String to, String subject, String content, String path);
   // 根據模版發送簡單郵件
  void sendMailByTemplate(String to, String subject, String templateName,
      Map<String, Object> params);
  
}

maven

鏡像

設置阿里雲鏡像,加快下載速度
修改 setting.xml,在 mirrors 節點上,添加

<mirror> 
    <id>alimaven</id> 
    <name>aliyun maven</name> 
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url> 
    <mirrorOf>central</mirrorOf> 
</mirror>

也能夠在項目 pom.xml 文件添加 ,僅當前項目有效

<repositories>
    <repository>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <releases>
        <enabled>true</enabled>
        <updatePolicy>daily</updatePolicy>
      </releases>
      <snapshots>
        <enabled>false</enabled>
        <checksumPolicy>warn</checksumPolicy>
      </snapshots>
      <layout>default</layout>
    </repository>
  </repositories>

總結

  1. spring boot 遵循開箱即用的原則,並不須要作過多配置,網上的教程質量良莠不齊,而且1.X和2.X使用時還有諸多不一樣,所以在使用時儘可能參考官方文檔
  2. 有時候默認的配置並不能知足咱們的需求,須要作一些自定義配置,推薦先看一下springboot自動配置的源碼,再作定製化處理
  3. 技術沒有銀彈,在作技術選型時不要過於迷信一種技術,適合本身的業務的技術纔是最好的
相關文章
相關標籤/搜索