Java開發技術大雜燴(三)之電商專案優化、rabbitmq、Git、OSI、VIM、Intellj IDEA、HTTP、JS、Java...

weixin_34128411發表於2018-06-05

前言

最近公司讓我維護Spring+Servlet+Hibernate+Spring Security+Jsp的老專案,正好可以鍛鍊我的業務邏輯和掌控專案的能力。雖然專案很老,但是其中還是有很多值我學習的地方。

電商專案優化

1.我們大致優化的點是秒殺介面:redis預減庫存,減少資料庫訪問;記憶體標記較少redis的訪問;rabbitmq佇列緩衝,非同步下單,增強使用者體驗。那麼具體步驟如下。

1.處理秒殺業務的Controller在Spring容器週期內載入就緒。也就是實現InitializingBean,在afterPropertiesSet()方法中把商品庫存載入到redis中,並且設定在記憶體中設定商品是否秒殺結束的flag。

    /**
     * 記憶體標記初始化
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        List<GoodsVo> goodsVoList = goodsService.listGoodsVo();

        if (CollectionUtils.isEmpty(goodsVoList)) {
            return;
        }

        goodsVoList.forEach(goodsVo -> {
            redisService.set(GoodsKey.getMiaoshaGoodsStock, "" + goodsVo.getId(), goodsVo.getStockCount());
            localOverMap.put(goodsVo.getId(), false);
        });
    }

2.後臺收到秒殺請求,首先檢視記憶體flag標記,然後減少redis中的商品庫存。如果商品秒殺結束,在記憶體中設定秒殺結束的flag。如果商品秒殺還在進行中,那麼進入下一步。

3.把秒殺商品的訊息進行入隊緩衝,直接返回。這裡並不是返回成功,而是返回到排隊中。此時,前臺不能直接提示秒殺成功,而是啟動定時器,過一段時間再去檢視是否成功。

4.訊息出隊,修改db中的庫存,建立秒殺訂單。

2.分散式Session的解決方案是生成唯一token,token標識使用者,把token寫到Cookie中,然後把token+使用者資訊寫進Redis,token在redis的失效時間要和Cookie失效時間保持一致。每當使用者登入一次,要延遲Session的有效期和Cookie有效期。

3.從快取的角度來說,我們可以進行頁面快取+URL快取+物件快取來達到優化的目的。我們可以手動渲染Thymeleaf模板,把商品詳情頁和商品列表頁快取到redis中,這裡用商品列表頁舉例。

    @RequestMapping(value = "/to_list", produces = "text/html;charset=UTF-8")
    @ResponseBody
    public String list(MiaoshaUser miaoshaUser) throws IOException {
        modelMap.addAttribute("user", miaoshaUser);
        //取快取
        String htmlCached = redisService.get(GoodsKey.getGoodsList, "", String.class);
        if (!StringUtils.isEmpty(htmlCached)) {
            return htmlCached;
        }
        List<GoodsVo> goodsVoList = goodsService.listGoodsVo();
        modelMap.addAttribute("goodsList", goodsVoList);
        SpringWebContext springWebContext = new SpringWebContext(request, response, request.getServletContext(),
                request.getLocale(), modelMap, applicationContext);
        String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext);

        if (!StringUtils.isEmpty(html)) {
            redisService.set(GoodsKey.getGoodsList, "", html);
        }
        return html;
    }

4.從靜態資源角度考慮,我們進行頁面靜態化、前後端分離、靜態資源優化、CDN節點優化。這裡用靜態資源優化舉例。

1.JS/CSS壓縮、減少流量。
2.多個JS/CSS組合,減少連線數
3.CDN就近訪問,減少請求時間。
4.將一些介面快取到使用者的瀏覽器中。

5.安全優化。密碼兩次加鹽,第一次加鹽是固定的,寫在Java程式碼的。第二次加鹽是隨機的,儲存在資料庫中。在商品秒殺頁,新增數學公式驗證碼,分散使用者的請求。對介面加入限流防刷機制。這裡以介面限流防刷機制舉例。

1.定義AccessLimit註解,作用於方法。

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

    int seconds();

    int maxCount();

    boolean needLogin() default true;
}

2.定義AccessInterceptor攔截器,獲得方法中AccessLimit註解中的引數。請求的reqeusturi作為redis中的key,seconds作為key的失效時間。每次請求加1,如果在指定時間內訪問該url的次數超過設定的maxCount,那麼返回“訪問太頻繁”。

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            MiaoshaUser user = getUser(request, response);
            UserContext.setUser(user);
            HandlerMethod hm = (HandlerMethod) handler;
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);

            if (Objects.isNull(accessLimit)) {
                return true;
            }
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();
            String key = request.getRequestURI();

            if (needLogin) {
                if (Objects.isNull(user)) {
                    render(response, CodeMsg.SESSION_ERROR);
                    return false;
                }
            }

            AccessKey ak = AccessKey.withExpire(seconds);
            Integer count = redisService.get(ak, key, Integer.class);

            if (Objects.isNull(count)) {
                redisService.set(ak, key, 1);
            } else if (count < maxCount) {
                redisService.incr(ak, key);
            } else {
                render(response, CodeMsg.ACCESS_LIMIT_REACHED);
                return false;
            }
        }
        return true;
    }

6.部署優化。LVS+Keepalived雙機熱備模式+Nginx+Tomcat。


Intelli J IDEA使用技巧

1.全域性搜尋 Ctrl + Shift + F
2.全域性替換 Ctrl +Shift + R


Vim編輯器使用技巧

1.在vim編輯器中進行查詢。

1.命令模式輸入“/字串”,例如"/xiaoma"
2.如果繼續查詢下一個,按n即可。


Redis設定密碼

1.因為在application-dev.properties中配置了spring.redis.password=,如果沒有在redis.conf沒有設定requirepass ${password},控制檯會丟擲連線拒絕異常。

HTTP

Cache Control的用法

no cache : 強制每次請求直接傳送給源伺服器,而不用經過本地快取版本的校驗。
max-age > 0 : 直接從瀏覽器快取中提取。


RabbitMQ

1.AMQP(Advance Message Queuing Protocol)是一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議。

2.Exchange在RabbitMQ中充當交換機的角色,也相當於路由。當然也可以形象的理解成RabbitMQ的過濾器。RabbitMQ有4種模式。


4636177-7674c15fe039235f.png
rabbitmq圖解-本圖來自於網際網路

1.Direct:按照Routing Key分到指定的Queue中。
2.Topic:和Direct差不多,但是可以多關鍵字匹配。
3.Fanout:無Routing Key概念,相當於廣播模式,將訊息分發給所有繫結FanoutExchange中的Queue。
4.Header:和上面3個不一樣,通過新增屬性key-value進行匹配。

3.編寫RabbitMQ程式碼

配置RabbitMQ的4種模式

/**
 * @author cmazxiaoma
 * @version V1.0
 * @Description: TODO
 * @date 2018/6/4 11:36
 */
@Configuration
public class MQConfig {

    public static final String MIAOSHA_QUEUE = "miaosha.queue";
    public static final String QUEUE = "queue";
    public static final String TOPIC_QUEUE1 = "topic.queue1";
    public static final String TOPIC_QUEUE2 = "topic.queue2";
    public static final String HEADER_QUEUE = "header.queue";
    public static final String TOPIC_EXCHANGE = "topicExchange";
    public static final String FANOUT_EXCHANGE = "fanoutExchange";
    public static final String HEADERS_EXCHANGE = "headersExchange";

    /**
     * Direct模式
     * @return
     */
    @Bean
    public Queue queue() {
        return new Queue(QUEUE, true);
    }

    @Bean
    public Queue miaoshaoQue() {
        return new Queue(MQConfig.MIAOSHA_QUEUE, true);
    }

    /**
     * Topic模式
     * @return
     */
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    @Bean
    public Queue topicQueue1() {
        return new Queue(TOPIC_QUEUE1, true);
    }

    @Bean
    public Queue topicQueue2() {
        return new Queue(TOPIC_QUEUE2, true);
    }

    @Bean
    public Binding topicBinding1() {
        return BindingBuilder
                .bind(topicQueue1())
                .to(topicExchange())
                .with("topic.key1");
    }

    @Bean
    public Binding topicBinding2() {
        return BindingBuilder
                .bind(topicQueue2())
                .to(topicExchange())
                .with("topic.#");
    }

    /**
     * Fanout模式
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE);
    }

    @Bean
    public Binding fanoutBinding1() {
        return BindingBuilder.bind(topicQueue1())
                .to(fanoutExchange());
    }

    @Bean
    public Binding fanoutBinding2() {
        return BindingBuilder.bind(topicQueue2())
                .to(fanoutExchange());
    }

    /**
     * Header模式
     * @return
     */
    @Bean
    public HeadersExchange headersExchange() {
        return new HeadersExchange(HEADERS_EXCHANGE);
    }

    @Bean
    public Queue headerQueue1() {
        return new Queue(HEADER_QUEUE, true);
    }

    @Bean
    public Binding headerBinding() {
        Map<String, Object> map = new HashMap<>();
        map.put("header1", "value1");
        map.put("header2", "value2");
        return BindingBuilder.bind(headerQueue1()).to(headersExchange())
                .whereAll(map).match();
    }
}

配置訊息生產者

/**
 * @author cmazxiaoma
 * @version V1.0
 * @Description: TODO
 * @date 2018/6/4 13:05
 */
@Service
@Slf4j
public class MQSender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMiaoshaMessageDirect(MiaoshaMessage miaoshaMessage) {
        String msg = RedisService.beanToString(miaoshaMessage);
        log.info("send direct message = {}", msg);
        amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
    }

    public void sendDirect(Object message) {
        String msg = RedisService.beanToString(message);
        log.info("send direct message = {}", msg);
        amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
    }

    public void sendTopic(Object message) {
        String msg = RedisService.beanToString(message);
        log.info("send topic message = {}", msg);
        amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg + "-1");
        amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg + "-2");
    }

    public void sendFanout(Object message) {
        String msg = RedisService.beanToString(message);
        log.info("send fanout message = {}", msg);
        amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);
    }

    public void sendHeader(Object message) {
        String msg = RedisService.beanToString(message);
        log.info("send header message = {}", msg);
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setHeader("header1", "value1");
        messageProperties.setHeader("header2", "value2");
        Message newMessage = new Message(msg.getBytes(), messageProperties);
        amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", newMessage);
    }

}

配置訊息消費者

/**
 * @author cmazxiaoma
 * @version V1.0
 * @Description: TODO
 * @date 2018/6/4 13:47
 */
@Service
@Slf4j
public class MQReceiver {

    @Autowired
    private RedisService redisService;

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private MiaoshaService miaoshaService;

    @RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
    public void receiveMiaoshaMessageDirect(String message) {
        log.info("receive direct miaosha message = {}", message);
        MiaoshaMessage miaoshaMessage = RedisService.stringToBean(message, MiaoshaMessage.class);
        MiaoshaUser miaoshaUser = miaoshaMessage.getMiaoshaUser();
        Long goodsId = miaoshaMessage.getGoodsId();
        GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goodsVo.getStockCount();

        if (stock <= 0) {
            return;
        }
        //判斷是否已經秒殺過
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(miaoshaUser.getId(), goodsId);

        if (!Objects.isNull(order)) {
            return;
        }
        //減庫存 下訂單 寫入秒殺訂單
        miaoshaService.miaosha(miaoshaUser, goodsVo);
    }

    @RabbitListener(queues = MQConfig.QUEUE)
    public void receiveDirect(String message) {
        log.info("receive direct message = {}", message);
    }

    @RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
    public void receiveTopic1(String message) {
        log.info("receive topic queue1 message = {}", message);
    }

    @RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
    public void receiveTopic2(String message) {
        log.info("receive topic queue2 message = {}", message);
    }

    @RabbitListener(queues = MQConfig.HEADER_QUEUE)
    public void receiveHeader(byte[] message) {
        log.info("receive header message = {}", new String(message));
    }
}

測試RabbitMQ的Controller

/**
 * @author cmazxiaoma
 * @version V1.0
 * @Description: TODO
 * @date 2018/5/29 16:36
 */
@Controller
@RequestMapping("/rabbitmq")
public class RabbitmqController extends BaseController {

    @Autowired
    private MQSender mqSender;

    @GetMapping("/header")
    @ResponseBody
    public Result<String> header() {
        mqSender.sendHeader("hello, header");
        return Result.success("hello, header");
    }

    @GetMapping("/fanout")
    @ResponseBody
    public Result<String> fanout() {
        mqSender.sendFanout("hello, fanout");
        return Result.success("hello, fanout");
    }

    @GetMapping("/topic")
    @ResponseBody
    public Result<String> topic() {
        mqSender.sendTopic("hello, topic");
        return Result.success("hello, topic");
    }

    @GetMapping("/direct")
    @ResponseBody
    public Result<String> direct() {
        mqSender.sendDirect("hello, direct");
        return Result.success("hello, direct");
    }
}

Nginx

Nginx的命令過一陣子不寫,老是忘記。還是記在簡書上面吧。
啟動:/usr/local/nginx/sbin/nginx -C /usr/local/nginx/conf/nginx.conf
關閉:/usr/local/nginx/sbin/nginx -s stop

我們在nginx.conf配置max_fail和fail_timeout引數,當失敗次數超過max_fail,nginx會把接下來的請求交給其他Real Server去處理。fail_timeout是失敗等待時間,當請求被認定失敗後,等待fail_timeout時間再去請求,判斷是否成功。


Git容易混淆的知識點

工作區:包括實際更改的檔案,當前修改還未add進入暫存區的檔案變化資訊。
暫存區:臨時儲存檔案的變化資訊

git reset filename:清空add命令向暫存區提交的關於filename檔案的修改。
git checkout --filename:撤銷對工作區的修改。


JS基礎知識

眾所周知,Java有三大特性:封裝,繼承,多型。我們可以用JS的protoType往java物件中注入這三大特性。

<script type="text/javascript">
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log("outer func:this.foo=" + this.foo);
        console.log("outer func:self.foo=" + self.foo);
        
        (function() {
            console.log("inner func:this.foo=" + this.foo);
            console.log("inner func:self.foo=" + self.foo);
        }());
        
    }
};
myObject.func();

Java = function() {};
Java.prototype = {
    oriented: function() {
        console.log("物件導向");
    },
    fengzhuang: function() {
        console.log("封裝");
    },
    extend: function() {
        console.log("繼承");
    }
};
java = new Java();
java.oriented();
java.fengzhuang();
java.extend();
</script>

Spring MVC冷門註解

1.produces="text/html"表示方法將產生“text/html”格式的資料,並且響應條的ContentType。我們在寫入訊息返回響應前,呼叫addDefaultHeaders()設定響應條中ContentType和ContentLength屬性。

    protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
        if (headers.getContentType() == null) {
            MediaType contentTypeToUse = contentType;
            if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
                contentTypeToUse = getDefaultContentType(t);
            }
            else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
                MediaType mediaType = getDefaultContentType(t);
                contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
            }
            if (contentTypeToUse != null) {
                if (contentTypeToUse.getCharset() == null) {
                    Charset defaultCharset = getDefaultCharset();
                    if (defaultCharset != null) {
                        contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
                    }
                }
                headers.setContentType(contentTypeToUse);
            }
        }
        if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
            Long contentLength = getContentLength(t, headers.getContentType());
            if (contentLength != null) {
                headers.setContentLength(contentLength);
            }
        }
    }

2.@ResponseBody該註解用於將Controller中方法的返回物件,根據HttpRequest中請求頭中Accept的內容,再通過合適的HttpMessageConverter轉換指定格式後,寫入到response物件(HttpOutputMessage)的body資料區中。若指定方法中consume為“application/json”,那麼方法僅處理請求頭中ContentType屬性值為"application/json"的請求。


4636177-49d1993a2fb792ee.png
image.png

3.判斷某個方法是否有指定的註解、某個方法所在的類上是否有指定的註解、某個方法的引數上是否有指定的註解。

parameter.hasParameterAnnotation(RequestBody.class)
AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class)
returnType.hasMethodAnnotation(ResponseBody.class)

4.@ModelAttribute的妙用

1.運用在方法的引數上,會將客戶端傳遞過來的引數按名稱注入到指定物件中,並且會將這個物件自動加入到modelMap中,便於view層呼叫

2.運用在方法中,會在每一個@RequestMapping標註的方法前執行,如果有返回值,則自動將該返回值加入modelMap中。我一般用於封裝BaseController

public abstract class BaseController {

    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected HttpSession session;
    protected ModelMap modelMap;

    @ModelAttribute
    protected void initSpringMvc(HttpServletRequest request, HttpServletResponse response,
                                         HttpSession session, ModelMap modelMap) {
        this.request = request;
        this.response = response;
        this.session = session;
        this.modelMap = modelMap;
    }
}

5.定時任務,我們在WebApplication類註解@EnableScheduling,開啟定時任務。cron表示式的引數從左到右分別是秒 、分、 時、 天、 月、 星期、 年。詳細的cron表示式用法請看這個網站http://cron.qqe2.com/

@Component
public class TestTask {

    @Scheduled(cron = "4-40 * * * * ?")
    public void reportCurrentTime() {
        System.out.println("現在時間:" + DateFormatUtils.format(new Date(),
                "yyyy-MM-dd HH:mm:ss"));
    }
}

6.開啟非同步任務,我們在WebApplication類註解@EnableAsync。

我們可以寫一個AsyncTask任務類

@Component
public class AsyncTask {

    @Async
    public Future<Boolean> doTask1() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("任務1耗時:" + (end - start));
        return new AsyncResult<>((true));
    }

    @Async
    public Future<Boolean> doTask2() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(2000);
        long end = System.currentTimeMillis();
        System.out.println("任務2耗時:" + (end - start));
        return new AsyncResult<>((true));
    }

    @Async
    public Future<Boolean> doTask3() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(3000);
        long end = System.currentTimeMillis();
        System.out.println("任務3耗時:" + (end - start));
        return new AsyncResult<>((true));
    }
}

然後在寫TaskController

@RestController
@RequestMapping("/tasks")
public class TaskController extends BaseController {

    @Autowired
    private AsyncTask asyncTask;

    @RequestMapping("test")
    public Result test() throws Exception {
        long start = System.currentTimeMillis();

        Future<Boolean> a = asyncTask.doTask1();
        Future<Boolean> b = asyncTask.doTask2();
        Future<Boolean> c = asyncTask.doTask3();

        while (!a.isDone() || !b.isDone() || !c.isDone()) {
            if (a.isDone() && b.isDone() && c.isDone()) {
                break;
            }
        }
        long end = System.currentTimeMillis();
        String times = "任務全部完成,總耗時:" + (end - start) + "毫秒";
        return Result.success(times);
    }
}

我們可以看到這3個任務總耗時是3000ms,證明任務是非同步執行的。如果去掉@Async,這3個任務執行是同步的,總耗時應該是6000多ms。

{"code":0,"data":"任務全部完成,總耗時:3005毫秒","msg":""}

7.SpringBoot部署到外部Tomcat,配置pom檔案。使tomcat作用域設定為provided,provided表明只在編譯器和測試時候使用,因為我們部署到外部Tomcat,執行期間有外部Tomcat的支援。

        <!--spring boot tomcat
        預設可以不用配置,但當需要把當前web應用佈置到外部servlet容器時就需要配置,
        並將scope配置為provided
        當需要預設的jar啟動,則去掉provided, provided表明只在編譯期和測試的時候使用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

記得把打包的方式從jar改成war

    <groupId>com.cmazxiaoma</groupId>
    <artifactId>seckillSystem</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

重寫SpringApplication這個啟動類,我這裡重新建立了一個類,名為WebApplication

@SpringBootApplication
//開啟定時任務
@EnableScheduling
//開啟非同步呼叫方法
@EnableAsync
public class WebApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WebApplication.class);
    }
}

然後Build Artifacts即可。


4636177-37b9bcd9b753e34e.png
image.png

OSI

OSI是開放式系統互聯,英文是Open System Interconnection

應用層
表示層
會話層
傳輸層
網路層
資料鏈路層
物理層

TCP/IP模型

應用層 =》 HTTP(超文字傳輸協議)、TFTP(簡單檔案傳輸協議)、SMTP(簡單郵件傳輸協議)、DNS(域名系統)、SNMP(簡單網路管理協議)、NFS(網路檔案系統)、Telnet(終端登入)
傳輸層 =》 TCP、IP
網路層 =》 IP、ICMP(國際控制報文協議)、ARP(地址解析協議)、RARP(反地址解析協議)
資料鏈路層 =》 PPP(點對點協議)


HttpMessageConverter所引發的異常

當我去請求/login/to_login會返回login檢視,login介面會去載入背景圖片。此時我們沒有去配置資源對映,導致背景圖片會請求後端的Controller。如果沒有找到合適的Controller去處理這個請求,會進入全域性異常捕獲器進入異常處理。在RequestResponseBodyMethodProcessor中的writeWithMessageConverters()方法中,我們會呼叫getProducibleMediaTypes()方法獲取該請求的所有返回訊息格式型別。

        HttpServletRequest request = inputMessage.getServletRequest();
        List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

        if (outputValue != null && producibleMediaTypes.isEmpty()) {
            throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
        }

由於我們沒有在全域性異常捕獲器HandlerMapping中顯式設定produces屬性,我們只能通過遍歷所有的HttpMessageConverter,通過canWrite()方法找到支援解析Java物件的HttpMessageConverter,並且把其所支援的mediaType加入mediaTypes集合裡面。

    protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
        Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<MediaType>(mediaTypes);
        }
        else if (!this.allSupportedMediaTypes.isEmpty()) {
            List<MediaType> result = new ArrayList<MediaType>();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                    if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                }
                else if (converter.canWrite(valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            return result;
        }
        else {
            return Collections.singletonList(MediaType.ALL);
        }
    }

我們得出producibleMediaTypes都是關於"application/json"的格式,我們for迴圈2次,將requestedMediaTypes和producibleMediaTypes一一比較,得出相容的compatibleMediaTypes。如果請求訊息格式和返回訊息格式沒有一個匹配的話,則丟擲HttpMediaTypeNotAcceptableException異常。


4636177-0eab104d59012b19.png
image.png
        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
        for (MediaType requestedType : requestedMediaTypes) {
            for (MediaType producibleType : producibleMediaTypes) {
                if (requestedType.isCompatibleWith(producibleType)) {
                    compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
                }
            }
        }
        if (compatibleMediaTypes.isEmpty()) {
            if (outputValue != null) {
                throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
            }
            return;
        }
解決辦法

在application-dev.properties檔案中配置靜態資源自動對映

spring.resources.add-mappings=true

或者是手動配置資源對映

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }

Java基礎知識

PreparedStatement物件有addBatch()、executeBatch()方法,用於批量插入。

        Connection conn = DBUtil.getConn();
        String sql = "insert into miaosha_user(login_count, nickname, register_date, salt, password, id)values(?,?,?,?,?,?)";
        PreparedStatement pstmt = conn.prepareStatement(sql);

        for (int i = 0; i < users.size(); i++) {
            MiaoshaUser user = users.get(i);
            pstmt.setInt(1, user.getLoginCount());
            pstmt.setString(2, user.getNickname());
            pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
            pstmt.setString(4, user.getSalt());
            pstmt.setString(5, user.getPassword());
            pstmt.setLong(6, user.getId());
            pstmt.addBatch();
        }
        pstmt.executeBatch();
        pstmt.close();
        conn.close();

isAssignableFrom()的用法,判斷Class1和Class2是否相同,判斷Class1是否是Class2的介面或者是其父類。

Class1.isAssignableFrom(Class2)

instance of 容易和isAssignableFrom()混淆,這用cmazxiaoma instance of Object舉例子,判斷一個物件例項是否是一個類、介面的例項,或者是其父類、子介面的例項

JSR303用法

JSR303是一個資料驗證的規範,這裡用手機號驗證舉例子

定義@IsMobile註解,這個註解要被IsMobileValidator類去實現驗證。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    boolean required() default true;

    String message() default "手機號碼格式錯誤";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

定義手機號驗證類,驗證沒通過會丟擲BindException

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    @Override
    public void initialize(IsMobile isMobile) {
        required = isMobile.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (required) {
            return ValidatorUtil.isMobile(value);
        } else {
            if (StringUtils.isEmpty(value)) {
                return true;
            } else {
                return ValidatorUtil.isMobile(value);
            }
        }
    }
}

驗證沒通過會丟擲BindException,我們在全域性異常捕獲器中捕獲這個異常。


@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Result<String> exceptionHandler(HttpServletRequest request, HttpServletResponse response,
                                           Object handler, Exception e) {
        log.error(e.getMessage());

        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.fillArgs(e.getMessage()));
        }
    }
}

尾言

每次逛部落格的時候,看到不懂的地方,一定要拿小本本記住。然後整理到簡書上面,日積月累,量變引發質變。

相關文章