代碼優化---拒絕寫死代碼
Controller 層返回值封裝
對返回結果封裝能夠有效的減少硬編碼。提升開發效率,方便維護。以前如果我們不對返回結構進行封裝就是直接 return 一個狀態碼啥的。實在不雅觀
Result
這是對返回結果封裝的一個類
package com.cpc.miaosha_02.result;
/**
* 這是類是對返回結果進行處理的類主要的目的是同樣返回的結果
* @param <T>
*/
public class Result<T> {
//狀態碼
private int code;
//說明
private String msg;
//實際數據
private T data;
/**
* 成功時候的調用
* */
public static <T> Result<T> success(T data){
return new Result<T>(data);
}
/**
* 失敗時候的調用
* */
public static <T> Result<T> error(CodeMsg codeMsg){
return new Result<T>(codeMsg);
}
private Result(T data) {
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
private Result(CodeMsg codeMsg) {
if(codeMsg != null) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
CodeMsg
這是對系統常量進行封裝,做到統一管理。防止個人硬編碼直接返回對應狀態碼,重構火葬場呀。這樣做好維護,好管理。
package com.cpc.miaosha_02.result;
import com.alibaba.fastjson.JSON;
/**
* 對應返回轉態做統一管理,以利於後期維護
*/
public class CodeMsg {
private int code;
private String msg;
//通用的錯誤碼
public static CodeMsg SUCCESS = new CodeMsg(0, "success");
public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服務端異常");
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "參數校驗異常:%s");
//登錄模塊 5002XX
public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已經失效");
public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登錄密碼不能爲空");
public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手機號不能爲空");
public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手機號格式錯誤");
public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手機號不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密碼錯誤");
//商品模塊 5003XX
//訂單模塊 5004XX
public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "訂單不存在");
//秒殺模塊 5005XX
public static CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已經秒殺完畢");
public static CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重複秒殺");
//將構造方法私有化,防止硬編碼
private CodeMsg( ) {
}
private CodeMsg( int code,String msg ) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
/**
* 對msg中的佔位符做處理
* @param args
* @return
*/
public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}
/**
* 使用例子
* @param args
*/
public static void main(String[] args) {
Result result = Result.error(CodeMsg.SESSION_ERROR);
System.out.println(JSON.toJSONString(result));
}
@Override
public String toString() {
return "CodeMsg [code=" + code + ", msg=" + msg + "]";
}
}
簡單的使用一下我們封裝好的類:
成功結果集我們可以這樣返回
/**
* 示例:使用封裝好的代碼來返回結果
* @return
*/
@RequestMapping(value="/detail/{goodsId}")
@ResponseBody
public Result<GoodsDetailVo> test() {
GoodsDetailVo vo = new GoodsDetailVo();
//這裏就簡單的示例子一下就不訪問數據庫了
return Result.success(vo);
}
如果操作過程中出現異常我們可以這樣做:
//這裏返回 在 CodeMsg 中定義好的常量,從根據上拒絕硬編碼
return Result.error(CodeMsg.PASSWORD_ERROR); //假設發生了異常
PASSWORD_ERROR 在這裏代表的是 ,密碼錯誤、狀態碼是 500215
你可以根據自己的情況進行擴展
JSR303 及 全局異常處理
jsr303 後端數據驗證
JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,官方參考實現是Hibernate Validator。
此實現與 Hibernate ORM 沒有任何關係。 JSR 303 用於對 Java Bean 中的字段的值進行驗證。
Spring MVC 3.x 之中也大力支持 JSR-303,可以在控制器中對錶單提交的數據方便地驗證。
注:可以使用註解的方式進行驗證
詳請參考:https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html
全局異常處理
系統中Controller總會因爲一些事情發送異常,這個時候我們就要進行異常處理。
傳統的做法是使用 try/catch 來玩,項目中輸不起的 處理方法,那麼就會有無數的 try/catch 代碼。顯然這是非常 冗餘的 代碼也就成了搬磚。
這時候我們是否能通過一種同樣的方式來對這些異常做同樣處理,響應個客戶端呢?當然有就是 全局異常處理
我們對登錄的實體類加上了如下註解:
package com.cpc.miaosha_02.vo;
import com.cpc.miaosha_02.validator.IsMobile;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
public class LoginVo {
@NotNull//不能爲空
@IsMobile //必須是手機號碼格式
private String mobile;
@NotNull
@Length(min=32) //指定密碼的長度最小必須是 32 個 字符
private String password;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
}
}
ps:IsMobile 這個是我們根據jsr303規範實現的自定義註解,是對jsr303的擴展。自定義代碼下面也會有。
在Controller 接受實體類的方法上加上 @Valid 這個註解就能實現驗證了:
@DisableToken
@RequestMapping("/do_login")
@ResponseBody
public Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登錄
String token = userService.login(response, loginVo);
return Result.success(token);
}
這就是基本的代碼實現驗證了,是不是和簡單,簡潔。下面我將剩餘的代碼附上:
這是我們自定義的註解實現驗證,不過要按照jsr303的套路來玩
package com.cpc.miaosha_02.validator;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
//自定義註解類:message() + groups() + payload() 必須;
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {
boolean required() default true;
String message() default "手機號碼格式錯誤";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
校驗器
package com.cpc.miaosha_02.validator;
import com.cpc.miaosha_02.util.ValidatorUtil;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
//註解校驗器類:繼承 ConstraintValidator 類<註解類,註解參數類型> + 兩個方法(initialize:初始化操作、isValid:邏輯處理)
//IsMobile:自定義的註解
//String:註解參數類型
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
//默認值_false,用於接收註解上自定義的 required
private boolean required = false;
//1、初始化方法:通過該方法我們可以拿到我們的註解
public void initialize(IsMobile constraintAnnotation) {
//constraintAnnotation.required() 接收我們自定義的屬性,是否運行爲空
required = constraintAnnotation.required();
}
//2、邏輯處理
public boolean isValid(String value, ConstraintValidatorContext context) {
//判斷是否運行爲空
if(required) {
return ValidatorUtil.isMobile(value);
}else {
//2.2、不允許爲空
//2.2.1、驗證是否爲空
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}
}
效驗工具類
package com.cpc.miaosha_02.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
public static boolean isMobile(String src) {
if(StringUtils.isEmpty(src)) {
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
}
自定義異常:
package com.cpc.miaosha_02.exception;
import com.cpc.miaosha_02.result.CodeMsg;
/**
*自定義異常類 繼承 RuntimeException(運行時異常類) 我們平臺通過 GlobalExceptionHandler 去處理
*/
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L;
private CodeMsg cm;
public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
}
}
下面是全局異常處理類,處理所以請求過程中發送的異常,啓動也包括對效驗失敗的處理。接和上面代碼:
package com.cpc.miaosha_02.exception;
import com.cpc.miaosha_02.result.CodeMsg;
import com.cpc.miaosha_02.result.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 全局請求異常處理類
*/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {//這是自定義異常的處理
GlobalException ex = (GlobalException)e;
//返回全局異常中的錯誤信息
return Result.error(ex.getCm());
}else if(e instanceof BindException) {//這是對 參數驗證錯誤異常的處理
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
//返回參數異常信息
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
//默認返回服務器異常
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
下面我可以測試一下:
這就是jsr303效驗配和全局異常處理,是不是比以前硬編碼舒服多了
通用的key生成策略
這是爲裏最項目中生成到redis中的key做集中化管理。易於項目的維護
KeyPrefix 接口
package com.javaxl.miaosha_02.redis;
public interface KeyPrefix {
public int expireSeconds();
public String getPrefix();
}
BasePrefix 基礎的 KeyPrefix 實現。所以同樣key配置的基類
package com.javaxl.miaosha_02.redis;
public abstract class BasePrefix implements KeyPrefix{
//這是key的過期秒數設置
private int expireSeconds;
//這是key的前綴
private String prefix;
//默認構造方式
public BasePrefix(String prefix) {//0代表永不過期
this(0, prefix);
}
public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
public int expireSeconds() {//默認0代表永不過期
return expireSeconds;
}
/**
* 前綴;返回的是 class類名+prefix
* @return
*/
public String getPrefix() {
String className = getClass().getSimpleName();
return className+":" + prefix;
}
}
MiaoshaUserKey 對應的也就是 MiaoshaUser key的生成策略
package com.javaxl.miaosha_02.redis;
public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE = 3600*24 * 2;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
public static MiaoshaUserKey getById = new MiaoshaUserKey(0, "id");
}
這裏配置了Goods對應的key生成方式
package com.javaxl.miaosha_02.redis;
public class GoodsKey extends BasePrefix{
private GoodsKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
//下面是配置的兩個通用key生成方式
public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
}
實用案例:
/**
* QPS:1267 load:15 mysql
* 5000 * 10
* QPS:2884, load:5
*
* 利用redis緩存整個商品列表
* */
@RequestMapping(value="/to_list", produces="text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
model.addAttribute("user", user);
//取緩存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if(!StringUtils.isEmpty(html)) {
return html;
}
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
// return "goods_list";
IWebContext ctx = new WebContext(request,response,
request.getServletContext(),request.getLocale(), model.asMap() );
//手動渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
生成的key這樣的:
通用的RedisService方法
避免出現重複的redis操作方法,讓操作redis變得簡單便捷
package com.javaxl.miaosha_02.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Service
public class RedisService {
@Autowired
JedisPool jedisPool;
/**
* 獲取當個對象
* */
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
String str = jedis.get(realKey);
T t = stringToBean(str, clazz);
return t;
}finally {
returnToPool(jedis);
}
}
/**
* 設置對象
* */
public <T> boolean set(KeyPrefix prefix, String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = beanToString(value);
if(str == null || str.length() <= 0) {
return false;
}
//生成真正的key
String realKey = prefix.getPrefix() + key;
int seconds = prefix.expireSeconds();
if(seconds <= 0) {
jedis.set(realKey, str);
}else {
jedis.setex(realKey, seconds, str);
}
return true;
}finally {
returnToPool(jedis);
}
}
/**
* 判斷key是否存在
* */
public <T> boolean exists(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.exists(realKey);
}finally {
returnToPool(jedis);
}
}
/**
* 刪除
* */
public boolean delete(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
long ret = jedis.del(key);
return ret > 0;
}finally {
returnToPool(jedis);
}
}
/**
* 增加值
* */
public <T> Long incr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.incr(realKey);
}finally {
returnToPool(jedis);
}
}
/**
* 減少值
* */
public <T> Long decr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.decr(realKey);
}finally {
returnToPool(jedis);
}
}
private <T> String beanToString(T value) {
if(value == null) {
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
}
@SuppressWarnings("unchecked")
private <T> T stringToBean(String str, Class<T> clazz) {
if(str == null || str.length() <= 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return (T)Integer.valueOf(str);
}else if(clazz == String.class) {
return (T)str;
}else if(clazz == long.class || clazz == Long.class) {
return (T)Long.valueOf(str);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}
private void returnToPool(Jedis jedis) {
if(jedis != null) {
jedis.close();
}
}
}
實用方式就是這樣的直接注入實用:
@Autowired
RedisService redisService;
md5前臺後臺兩次加鹽加密
會有兩次加密:
一次:客戶端密碼加密,防止明文密碼被劫持
二次:服務端再加密一次
前臺加密要導入 的加油工具包:
鏈接:https://pan.baidu.com/s/1vR6T1CkZXGYoqJk5UssoeA
提取碼:q2bd
貼上前臺js加密代碼:
//鹽(這裏就不做隨機鹽了,有興趣的可以自行研究擴展)
var g_passsword_salt="1a2b3c4d"
function doLogin(){
g_showLoading();
var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
//對前臺密碼進行md5加鹽加密
var password = md5(str);
$.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
貼上後臺代碼:
public String login(HttpServletResponse response, LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
//前臺傳入的密碼是已經加過密的 對於安全性來說是非常好的選擇
String formPass = loginVo.getPassword();
//判斷手機號是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
//拋出異常
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//驗證密碼
String dbPass = user.getPassword();
String saltDB = user.getSalt();
if(!PasswordHelper.checkCredentials(formPass, saltDB, dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return token;
}
這裏做的是登錄的,註冊就是將前臺加密後的密碼,再次加鹽加密。只不過加的是隨機鹽而已